Compare commits
70 Commits
db-mongodb
...
db-postgre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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"]
|
||||
}
|
||||
47
.github/dependabot.yml
vendored
Normal file
47
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
dev:
|
||||
dependency-type: development
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
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/*'
|
||||
25
.github/workflows/lock-issues.yml
vendored
Normal file
25
.github/workflows/lock-issues.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
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'
|
||||
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/
|
||||
|
||||
64
CHANGELOG.md
64
CHANGELOG.md
@@ -1,3 +1,67 @@
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -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._
|
||||
|
||||
|
||||
@@ -71,3 +71,104 @@ 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) |
|
||||
@@ -167,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.7.1",
|
||||
"version": "1.7.2",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,32 @@ export const sanitizeQueryValue = ({
|
||||
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'exists') {
|
||||
formattedValue = formattedValue === 'true' || formattedValue === true
|
||||
|
||||
if (formattedValue) {
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [path]: { $exists: true } },
|
||||
{ [path]: { $ne: null } },
|
||||
{ [path]: { $ne: '' } },
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rawQuery: {
|
||||
$or: [
|
||||
{ [path]: { $exists: false } },
|
||||
{ [path]: { $eq: null } },
|
||||
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.25.0",
|
||||
"version": "2.29.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -73,7 +73,7 @@
|
||||
"deep-equal": "2.2.2",
|
||||
"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,7 +96,7 @@
|
||||
"is-plain-object": "5.0.0",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"joi": "17.9.2",
|
||||
"json-schema-to-typescript": "11.0.3",
|
||||
"json-schema-to-typescript": "14.0.5",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"jwt-decode": "3.1.2",
|
||||
"md5": "2.3.0",
|
||||
@@ -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,7 +129,7 @@
|
||||
"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",
|
||||
@@ -199,7 +199,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ const ArrayCell: React.FC<CellComponentProps<ArrayField, Record<string, unknown>
|
||||
}) => {
|
||||
const { i18n, t } = useTranslation('general')
|
||||
const arrayFields = data ?? []
|
||||
const label = `${arrayFields.length} ${getTranslation(field?.labels?.plural || t('rows'), i18n)}`
|
||||
|
||||
const label =
|
||||
arrayFields.length === 1
|
||||
? `${arrayFields.length} ${getTranslation(field?.labels?.singular || t('row'), i18n)}`
|
||||
: `${arrayFields.length} ${getTranslation(field?.labels?.plural || t('rows'), i18n)}`
|
||||
|
||||
return <span>{label}</span>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { RelationshipField } from '../../../../../../../../exports/types'
|
||||
import type { RelationshipField, UploadField } from '../../../../../../../../exports/types'
|
||||
import type { CellComponentProps } from '../../types'
|
||||
|
||||
import { getTranslation } from '../../../../../../../../utilities/getTranslation'
|
||||
@@ -9,13 +9,14 @@ import useIntersect from '../../../../../../../hooks/useIntersect'
|
||||
import { formatUseAsTitle } from '../../../../../../../hooks/useTitle'
|
||||
import { useConfig } from '../../../../../../utilities/Config'
|
||||
import { useListRelationships } from '../../../RelationshipProvider'
|
||||
import File from '../File'
|
||||
import './index.scss'
|
||||
|
||||
type Value = { relationTo: string; value: number | string }
|
||||
const baseClass = 'relationship-cell'
|
||||
const totalToShow = 3
|
||||
|
||||
const RelationshipCell: React.FC<CellComponentProps<RelationshipField>> = (props) => {
|
||||
const RelationshipCell: React.FC<CellComponentProps<RelationshipField | UploadField>> = (props) => {
|
||||
const { data: cellData, field } = props
|
||||
const config = useConfig()
|
||||
const { collections, routes } = config
|
||||
@@ -68,11 +69,24 @@ const RelationshipCell: React.FC<CellComponentProps<RelationshipField>> = (props
|
||||
i18n,
|
||||
})
|
||||
|
||||
let fileField = null
|
||||
if (field.type === 'upload') {
|
||||
const relatedCollectionPreview = !!relatedCollection.upload.displayPreview
|
||||
const fieldPreview = field.displayPreview
|
||||
const previewAllowed =
|
||||
fieldPreview || (relatedCollectionPreview && fieldPreview !== false)
|
||||
if (previewAllowed && document) {
|
||||
fileField = (
|
||||
<File collection={relatedCollection} data={label} field={field} rowData={document} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{document === false && `${t('untitled')} - ID: ${value}`}
|
||||
{document === null && `${t('loading')}...`}
|
||||
{document && (label || `${t('untitled')} - ID: ${value}`)}
|
||||
{document && (fileField || label || `${t('untitled')} - ID: ${value}`)}
|
||||
{values.length > i + 1 && ', '}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass} ${baseClass}--${collection.slug}`}>
|
||||
{Array.isArray(BeforeList) &&
|
||||
BeforeList.map((Component, i) => <Component key={i} {...props} />)}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export const registerLocalStrategy = async ({
|
||||
const sanitizedDoc = { ...doc }
|
||||
if (sanitizedDoc.password) delete sanitizedDoc.password
|
||||
|
||||
return payload.db.create({
|
||||
const dbArgs = {
|
||||
collection: collection.slug,
|
||||
data: {
|
||||
...sanitizedDoc,
|
||||
@@ -49,5 +49,10 @@ export const registerLocalStrategy = async ({
|
||||
salt,
|
||||
},
|
||||
req,
|
||||
})
|
||||
}
|
||||
if (collection?.db?.create) {
|
||||
return collection.db.create(dbArgs)
|
||||
} else {
|
||||
return payload.db.create(dbArgs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ const collectionSchema = joi.object().keys({
|
||||
joi.boolean(),
|
||||
),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
db: joi.object(),
|
||||
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
defaultSort: joi.string(),
|
||||
endpoints: endpointsSchema,
|
||||
@@ -171,6 +172,7 @@ const collectionSchema = joi.object().keys({
|
||||
adminThumbnail: joi.alternatives().try(joi.string(), joi.func()),
|
||||
crop: joi.bool(),
|
||||
disableLocalStorage: joi.bool(),
|
||||
displayPreview: joi.bool().default(false),
|
||||
externalFileHeaderFilter: joi.func(),
|
||||
filesRequiredOnCreate: joi.bool(),
|
||||
focalPoint: joi.bool(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Response } from 'express'
|
||||
import type { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from 'graphql'
|
||||
import type { DeepRequired } from 'ts-essentials'
|
||||
|
||||
import type { GeneratedTypes } from '../../'
|
||||
import type { DatabaseAdapter, GeneratedTypes } from '../../'
|
||||
import type {
|
||||
CustomPreviewButtonProps,
|
||||
CustomPublishButtonType,
|
||||
@@ -383,6 +383,14 @@ export type CollectionConfig = {
|
||||
auth?: IncomingAuthType | boolean
|
||||
/** Extension point to add your custom data. */
|
||||
custom?: Record<string, any>
|
||||
|
||||
/**
|
||||
* Add a custom database adapter to this collection.
|
||||
*/
|
||||
db?: Pick<
|
||||
DatabaseAdapter,
|
||||
'create' | 'deleteMany' | 'deleteOne' | 'find' | 'findOne' | 'updateOne'
|
||||
>
|
||||
/**
|
||||
* Used to override the default naming of the database table or collection with your using a function or string
|
||||
* @WARNING: If you change this property with existing data, you will need to handle the renaming of the table in your database or by using migrations
|
||||
|
||||
@@ -242,11 +242,16 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
req,
|
||||
})
|
||||
} else {
|
||||
doc = await payload.db.create({
|
||||
const dbArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
data: resultWithLocales,
|
||||
req,
|
||||
})
|
||||
}
|
||||
if (collectionConfig?.db?.create) {
|
||||
doc = await collectionConfig.db.create(dbArgs)
|
||||
} else {
|
||||
doc = await payload.db.create(dbArgs)
|
||||
}
|
||||
}
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
@@ -104,12 +104,20 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
|
||||
// Retrieve documents
|
||||
// /////////////////////////////////////
|
||||
|
||||
const { docs } = await payload.db.find<GeneratedTypes['collections'][TSlug]>({
|
||||
const dbArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
locale,
|
||||
req,
|
||||
where: fullWhere,
|
||||
})
|
||||
}
|
||||
let docs
|
||||
if (collectionConfig?.db?.find) {
|
||||
const result = await collectionConfig.db.find<GeneratedTypes['collections'][TSlug]>(dbArgs)
|
||||
docs = result.docs
|
||||
} else {
|
||||
const result = await payload.db.find<GeneratedTypes['collections'][TSlug]>(dbArgs)
|
||||
docs = result.docs
|
||||
}
|
||||
|
||||
const errors = []
|
||||
|
||||
@@ -160,7 +168,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
|
||||
// Delete document
|
||||
// /////////////////////////////////////
|
||||
|
||||
await payload.db.deleteOne({
|
||||
const deleteOneArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
req,
|
||||
where: {
|
||||
@@ -168,7 +176,12 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if (collectionConfig?.db?.deleteOne) {
|
||||
await collectionConfig.db.deleteOne(deleteOneArgs)
|
||||
} else {
|
||||
await payload.db.deleteOne(deleteOneArgs)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Fields
|
||||
|
||||
@@ -96,13 +96,19 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// /////////////////////////////////////
|
||||
// Retrieve document
|
||||
// /////////////////////////////////////
|
||||
|
||||
const docToDelete = await req.payload.db.findOne({
|
||||
let docToDelete: Document
|
||||
const dbArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
locale: req.locale,
|
||||
req,
|
||||
where: combineQueries({ id: { equals: id } }, accessResults),
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig?.db?.findOne) {
|
||||
docToDelete = await collectionConfig.db.findOne(dbArgs)
|
||||
} else {
|
||||
docToDelete = await req.payload.db.findOne(dbArgs)
|
||||
}
|
||||
|
||||
if (!docToDelete && !hasWhereAccess) throw new NotFound(t)
|
||||
if (!docToDelete && hasWhereAccess) throw new Forbidden(t)
|
||||
@@ -132,11 +138,17 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// Delete document
|
||||
// /////////////////////////////////////
|
||||
|
||||
let result = await req.payload.db.deleteOne({
|
||||
let result
|
||||
const deleteOneArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
req,
|
||||
where: { id: { equals: id } },
|
||||
})
|
||||
}
|
||||
if (collectionConfig?.db?.deleteOne) {
|
||||
result = await collectionConfig?.db.deleteOne(deleteOneArgs)
|
||||
} else {
|
||||
result = await payload.db.deleteOne(deleteOneArgs)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Delete Preferences
|
||||
|
||||
@@ -142,7 +142,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
where,
|
||||
})
|
||||
|
||||
result = await payload.db.find<T>({
|
||||
const dbArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
limit: sanitizedLimit,
|
||||
locale,
|
||||
@@ -151,7 +151,13 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
req,
|
||||
sort,
|
||||
where: fullWhere,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig?.db?.find) {
|
||||
result = await collectionConfig.db.find<T>(dbArgs)
|
||||
} else {
|
||||
result = await payload.db.find<T>(dbArgs)
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -87,7 +87,12 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
|
||||
|
||||
if (!findOneArgs.where.and[0].id) throw new NotFound(t)
|
||||
|
||||
let result: T = await req.payload.db.findOne(findOneArgs)
|
||||
let result: T
|
||||
if (collectionConfig?.db?.findOne) {
|
||||
result = await collectionConfig.db.findOne(findOneArgs)
|
||||
} else {
|
||||
result = await req.payload.db.findOne(findOneArgs)
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
if (!disableErrors) {
|
||||
|
||||
@@ -85,7 +85,12 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
|
||||
where: combineQueries({ id: { equals: parentDocID } }, accessResults),
|
||||
}
|
||||
|
||||
const doc = await req.payload.db.findOne(findOneArgs)
|
||||
let doc: T
|
||||
if (collectionConfig?.db?.findOne) {
|
||||
doc = await collectionConfig.db.findOne(findOneArgs)
|
||||
} else {
|
||||
doc = await req.payload.db.findOne(findOneArgs)
|
||||
}
|
||||
|
||||
if (!doc && !hasWherePolicy) throw new NotFound(t)
|
||||
if (!doc && hasWherePolicy) throw new Forbidden(t)
|
||||
@@ -106,12 +111,18 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
|
||||
// Update
|
||||
// /////////////////////////////////////
|
||||
|
||||
let result = await req.payload.db.updateOne({
|
||||
const restoreVersionArgs = {
|
||||
id: parentDocID,
|
||||
collection: collectionConfig.slug,
|
||||
data: rawVersion.version,
|
||||
req,
|
||||
})
|
||||
}
|
||||
let result
|
||||
if (collectionConfig?.db?.updateOne) {
|
||||
result = await collectionConfig.db.updateOne(restoreVersionArgs)
|
||||
} else {
|
||||
result = await req.payload.db.updateOne(restoreVersionArgs)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Save `previousDoc` as a version after restoring
|
||||
|
||||
@@ -137,14 +137,21 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
|
||||
docs = query.docs
|
||||
} else {
|
||||
const query = await payload.db.find({
|
||||
const dbArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
limit: 0,
|
||||
locale,
|
||||
pagination: false,
|
||||
req,
|
||||
where: fullWhere,
|
||||
})
|
||||
}
|
||||
|
||||
let query
|
||||
if (collectionConfig?.db?.find) {
|
||||
query = await collectionConfig.db.find(dbArgs)
|
||||
} else {
|
||||
query = await payload.db.find(dbArgs)
|
||||
}
|
||||
|
||||
docs = query.docs
|
||||
}
|
||||
@@ -282,13 +289,18 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (!shouldSaveDraft || data._status === 'published') {
|
||||
result = await req.payload.db.updateOne({
|
||||
const dbArgs = {
|
||||
id,
|
||||
collection: collectionConfig.slug,
|
||||
data: result,
|
||||
locale,
|
||||
req,
|
||||
})
|
||||
}
|
||||
if (collectionConfig?.db?.updateOne) {
|
||||
result = await collectionConfig.db.updateOne(dbArgs)
|
||||
} else {
|
||||
result = await req.payload.db.updateOne(dbArgs)
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -91,9 +91,9 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
}
|
||||
|
||||
let { data } = args
|
||||
const { password } = data
|
||||
const dataHasPassword = 'password' in data && data.password
|
||||
const shouldSaveDraft = Boolean(draftArg && collectionConfig.versions.drafts)
|
||||
const shouldSavePassword = Boolean(password && collectionConfig.auth && !shouldSaveDraft)
|
||||
const shouldSavePassword = Boolean(dataHasPassword && collectionConfig.auth && !shouldSaveDraft)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Access
|
||||
@@ -256,7 +256,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// /////////////////////////////////////
|
||||
|
||||
const dataToUpdate: Record<string, unknown> = { ...result }
|
||||
|
||||
const { password } = dataToUpdate
|
||||
if (shouldSavePassword && typeof password === 'string') {
|
||||
const { hash, salt } = await generatePasswordSaltHash({ password })
|
||||
dataToUpdate.salt = salt
|
||||
@@ -270,13 +270,18 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (!shouldSaveDraft || data._status === 'published') {
|
||||
result = await req.payload.db.updateOne({
|
||||
const dbArgs = {
|
||||
id,
|
||||
collection: collectionConfig.slug,
|
||||
data: dataToUpdate,
|
||||
locale,
|
||||
req,
|
||||
})
|
||||
}
|
||||
if (collectionConfig?.db?.updateOne) {
|
||||
result = await collectionConfig.db.updateOne(dbArgs)
|
||||
} else {
|
||||
result = await req.payload.db.updateOne(dbArgs)
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -39,6 +39,7 @@ export const defaults: Omit<Config, 'db' | 'editor'> = {
|
||||
schemaOutputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/schema.graphql`,
|
||||
},
|
||||
hooks: {},
|
||||
joiValidation: true,
|
||||
localization: false,
|
||||
maxDepth: 10,
|
||||
rateLimit: {
|
||||
|
||||
@@ -128,6 +128,7 @@ export default joi.object({
|
||||
}),
|
||||
i18n: joi.object(),
|
||||
indexSortableFields: joi.boolean(),
|
||||
joiValidation: joi.boolean(),
|
||||
local: joi.boolean(),
|
||||
localization: joi.alternatives().try(
|
||||
joi.object().keys({
|
||||
|
||||
@@ -149,6 +149,11 @@ export type InitOptions = {
|
||||
*/
|
||||
local?: boolean
|
||||
|
||||
/**
|
||||
* A previously instantiated logger instance. Must conform to the PayloadLogger interface which uses Pino
|
||||
* This allows you to bring your own logger instance and let payload use it
|
||||
*/
|
||||
logger?: PayloadLogger
|
||||
loggerDestination?: DestinationStream
|
||||
/**
|
||||
* Specify options for the built-in Pino logger that Payload uses for internal logging.
|
||||
@@ -156,11 +161,6 @@ export type InitOptions = {
|
||||
* See Pino Docs for options: https://getpino.io/#/docs/api?id=options
|
||||
*/
|
||||
loggerOptions?: LoggerOptions
|
||||
/**
|
||||
* A previously instantiated logger instance. Must conform to the PayloadLogger interface which uses Pino
|
||||
* This allows you to bring your own logger instance and let payload use it
|
||||
*/
|
||||
logger?: PayloadLogger
|
||||
|
||||
/**
|
||||
* A function that is called immediately following startup that receives the Payload instance as it's only argument.
|
||||
@@ -635,6 +635,12 @@ export type Config = {
|
||||
i18n?: i18nInitOptions
|
||||
/** Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. */
|
||||
indexSortableFields?: boolean
|
||||
/**
|
||||
* Disable JOI validation
|
||||
*
|
||||
* @default true // enabled by default
|
||||
*/
|
||||
joiValidation?: boolean
|
||||
/**
|
||||
* Translate your content to different languages/locales.
|
||||
*
|
||||
|
||||
@@ -83,6 +83,10 @@ const validateSchema = async (
|
||||
abortEarly: false,
|
||||
})
|
||||
|
||||
if (!config?.joiValidation) {
|
||||
return config
|
||||
}
|
||||
|
||||
const nestedErrors = [
|
||||
...(await validateCollections(config.collections)),
|
||||
...validateGlobals(config.globals),
|
||||
|
||||
@@ -109,7 +109,8 @@ export async function getLocalizedPaths({
|
||||
if (typeof matchedField.relationTo !== 'string') {
|
||||
const lastSegmentIsValid =
|
||||
['relationTo', 'value'].includes(pathSegments[pathSegments.length - 1]) ||
|
||||
pathSegments.length === 1
|
||||
pathSegments.length === 1 ||
|
||||
(pathSegments.length === 2 && pathSegments[0] === 'version')
|
||||
|
||||
if (lastSegmentIsValid) {
|
||||
lastIncompletePath.complete = true
|
||||
|
||||
@@ -342,6 +342,7 @@ export const upload = baseField.keys({
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
|
||||
displayPreview: joi.boolean().default(false),
|
||||
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
|
||||
maxDepth: joi.number(),
|
||||
relationTo: joi.string().required(),
|
||||
|
||||
@@ -128,12 +128,13 @@ export type Labels = {
|
||||
singular: Record<string, string> | string
|
||||
}
|
||||
|
||||
export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
|
||||
export type ValidateOptions<TData, TSiblingData, TFieldConfig, TValue> = {
|
||||
config: SanitizedConfig
|
||||
data: Partial<TData>
|
||||
id?: number | string
|
||||
operation?: Operation
|
||||
payload?: Payload
|
||||
previousValue?: TValue
|
||||
req?: PayloadRequest
|
||||
siblingData: Partial<TSiblingData>
|
||||
t: TFunction
|
||||
@@ -143,7 +144,7 @@ export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
|
||||
// TODO: Having TFieldConfig as any breaks all type checking / auto-completions for the base ValidateOptions properties.
|
||||
export type Validate<TValue = any, TData = any, TSiblingData = any, TFieldConfig = any> = (
|
||||
value: TValue,
|
||||
options: ValidateOptions<TData, TSiblingData, TFieldConfig>,
|
||||
options: ValidateOptions<TData, TSiblingData, TFieldConfig, TValue>,
|
||||
) => Promise<string | true> | string | true
|
||||
|
||||
export type OptionObject = {
|
||||
@@ -407,6 +408,7 @@ export type UploadField = FieldBase & {
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
}
|
||||
displayPreview?: boolean
|
||||
filterOptions?: FilterOptions
|
||||
maxDepth?: number
|
||||
relationTo: string
|
||||
|
||||
@@ -125,24 +125,25 @@ const relationshipPopulationPromise = async ({
|
||||
|
||||
if (fieldSupportsMany(field) && field.hasMany) {
|
||||
if (
|
||||
field.localized &&
|
||||
locale === 'all' &&
|
||||
typeof siblingDoc[field.name] === 'object' &&
|
||||
siblingDoc[field.name] !== null
|
||||
) {
|
||||
Object.keys(siblingDoc[field.name]).forEach((key) => {
|
||||
if (Array.isArray(siblingDoc[field.name][key])) {
|
||||
siblingDoc[field.name][key].forEach((relatedDoc, index) => {
|
||||
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
|
||||
if (Array.isArray(siblingDoc[field.name][localeKey])) {
|
||||
siblingDoc[field.name][localeKey].forEach((relatedDoc, index) => {
|
||||
const rowPromise = async () => {
|
||||
await populate({
|
||||
currentDepth,
|
||||
data: siblingDoc[field.name][key][index],
|
||||
data: siblingDoc[field.name][localeKey][index],
|
||||
dataReference: resultingDoc,
|
||||
depth: populateDepth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
index,
|
||||
key,
|
||||
key: localeKey,
|
||||
locale,
|
||||
overrideAccess,
|
||||
req,
|
||||
@@ -178,21 +179,22 @@ const relationshipPopulationPromise = async ({
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
field.localized &&
|
||||
locale === 'all' &&
|
||||
typeof siblingDoc[field.name] === 'object' &&
|
||||
siblingDoc[field.name] !== null &&
|
||||
locale === 'all'
|
||||
siblingDoc[field.name] !== null
|
||||
) {
|
||||
Object.keys(siblingDoc[field.name]).forEach((key) => {
|
||||
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
|
||||
const rowPromise = async () => {
|
||||
await populate({
|
||||
currentDepth,
|
||||
data: siblingDoc[field.name][key],
|
||||
data: siblingDoc[field.name][localeKey],
|
||||
dataReference: resultingDoc,
|
||||
depth: populateDepth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
key,
|
||||
key: localeKey,
|
||||
locale,
|
||||
overrideAccess,
|
||||
req,
|
||||
|
||||
@@ -2,36 +2,36 @@
|
||||
"$schema": "./translation-schema.json",
|
||||
"authentication": {
|
||||
"account": "Račun",
|
||||
"accountOfCurrentUser": "Račun od trenutnog korisnika",
|
||||
"accountOfCurrentUser": "Račun trenutnog korisnika",
|
||||
"alreadyActivated": "Već aktivirano",
|
||||
"alreadyLoggedIn": "Već prijavljen",
|
||||
"alreadyLoggedIn": "Već prijavljeni",
|
||||
"apiKey": "API ključ",
|
||||
"authenticated": "Autenticiran",
|
||||
"backToLogin": "Nazad na prijavu",
|
||||
"beginCreateFirstUser": "Za početak, kreiraj svog prvog korisnika.",
|
||||
"backToLogin": "Natrag na prijavu",
|
||||
"beginCreateFirstUser": "Za početak, kreirajte prvog korisnika.",
|
||||
"changePassword": "Promjeni lozinku",
|
||||
"checkYourEmailForPasswordReset": "Provjerite email s poveznicom koja će Vam omogućiti sigurnu promjenu lozinke.",
|
||||
"confirmGeneration": "Potvrdi kreiranje",
|
||||
"checkYourEmailForPasswordReset": "Provjerite e-mail s poveznicom koja će vam omogućiti sigurnu promjenu lozinke.",
|
||||
"confirmGeneration": "Potvrdi generiranje",
|
||||
"confirmPassword": "Potvrdi lozinku",
|
||||
"createFirstUser": "Kreiraj prvog korisnika",
|
||||
"emailNotValid": "Email nije ispravan",
|
||||
"emailSent": "Email poslan",
|
||||
"emailNotValid": "E-mail adresa nije ispravna",
|
||||
"emailSent": "E-mail poslan",
|
||||
"enableAPIKey": "Omogući API ključ",
|
||||
"failedToUnlock": "Neuspješno otključavanje.",
|
||||
"failedToUnlock": "Otključavanje nije uspjelo.",
|
||||
"forceUnlock": "Prisilno otključaj",
|
||||
"forgotPassword": "Zaboravljena lozinka",
|
||||
"forgotPasswordEmailInstructions": "Molim unesite svoj email. Primit ćete poruku s uputama za ponovno postavljanje lozinke.",
|
||||
"forgotPasswordEmailInstructions": "Molimo unesite svoju e-mail adresu. Primit ćete poruku s uputama za ponovno postavljanje lozinke.",
|
||||
"forgotPasswordQuestion": "Zaboravljena lozinka?",
|
||||
"generate": "Generiraj",
|
||||
"generateNewAPIKey": "Generiraj novi API ključ",
|
||||
"generatingNewAPIKeyWillInvalidate": "Generiranje novog API ključa će <1>poništiti</1> prethodni ključ. Jeste li sigurni da želite nastaviti?",
|
||||
"lockUntil": "Zaključaj dok",
|
||||
"logBackIn": "Ponovna prijava",
|
||||
"logBackIn": "Ponovo se prijavite",
|
||||
"logOut": "Odjava",
|
||||
"loggedIn": "Za prijavu s drugim korisničkim računom potrebno je prvo <0>odjaviti se</0>",
|
||||
"loggedInChangePassword": "Da biste promijenili lozinku, otvorite svoj <0>račun</0> i promijenite lozinku tamo.",
|
||||
"loggedOutInactivity": "Odjavljeni se zbog neaktivnosti.",
|
||||
"loggedOutSuccessfully": "Uspješno ste odjavljeni..",
|
||||
"loggedIn": "Za prijavu s drugim korisničkim računom potrebno se prvo <0>odjaviti</0>",
|
||||
"loggedInChangePassword": "Da biste promijenili lozinku, otvorite svoj <0>račun</0> i promijenite je tamo.",
|
||||
"loggedOutInactivity": "Odjavljeni ste zbog neaktivnosti.",
|
||||
"loggedOutSuccessfully": "Uspješno ste odjavljeni.",
|
||||
"login": "Prijava",
|
||||
"loginAttempts": "Pokušaji prijave",
|
||||
"loginUser": "Prijava korisnika",
|
||||
@@ -39,32 +39,32 @@
|
||||
"logout": "Odjava",
|
||||
"logoutUser": "Odjava korisnika",
|
||||
"newAPIKeyGenerated": "Novi API ključ generiran.",
|
||||
"newAccountCreated": "Novi račun je kreiran. Pristupite računu klikom na <a href=\"{{serverURL}}\">{{serverURL}}</a>. Molim kliknite na sljedeći link ili zalijepite URL, koji se nalazi ispod, u preglednik da biste potvrdili svoj email: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Nakon što potvrdite email, moći ćete se prijaviti.",
|
||||
"newAccountCreated": "Novi račun je kreiran. Pristupite računu klikom na: <a href=\"{{serverURL}}\">{{serverURL}}</a>. Molimo kliknite na sljedeću poveznicu ili zalijepite URL, koji se nalazi ispod, u preglednik da biste potvrdili svoju e-mail adresu: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Nakon što potvrdite e-mail adresu, moći ćete se prijaviti.",
|
||||
"newPassword": "Nova lozinka",
|
||||
"resetPassword": "Restartiranje lozinke",
|
||||
"resetPasswordExpiration": "Restartiranje roka trajanja lozinke",
|
||||
"resetPasswordToken": "Restartiranje lozinke tokena",
|
||||
"resetYourPassword": "Restartiraj svoju lozinku",
|
||||
"stayLoggedIn": "Ostani prijavljen",
|
||||
"resetPassword": "Resetiranje lozinke",
|
||||
"resetPasswordExpiration": "Rok trajanja resetiranja lozinke",
|
||||
"resetPasswordToken": "Resetiranje lozinke tokena",
|
||||
"resetYourPassword": "Resetirajte svoju lozinku",
|
||||
"stayLoggedIn": "Ostanite prijavljeni",
|
||||
"successfullyUnlocked": "Uspješno otključano",
|
||||
"unableToVerify": "Nije moguće potvrditi",
|
||||
"verified": "Potvrđeno",
|
||||
"verifiedSuccessfully": "Uspješno potvrđeno",
|
||||
"verify": "Potvrdi",
|
||||
"verifyUser": "Potvrdi korisnika",
|
||||
"verifyYourEmail": "Potvrdi svoj email",
|
||||
"verifyYourEmail": "Potvrdi svoju e-mail adresu",
|
||||
"youAreInactive": "Neaktivni ste neko vrijeme i uskoro ćete biti automatski odjavljeni zbog vlastite sigurnosti. Želite li ostati prijavljeni?",
|
||||
"youAreReceivingResetPassword": "Primili ste ovo jer ste Vi (ili netko drugi) zatražili promjenu lozinke za Vaš račun. Molim kliknite na poveznicu ili zalijepite ovo u svoje preglednik da biste završili proces:",
|
||||
"youDidNotRequestPassword": "Ako niste zatražili ovo, molim ignorirajte ovaj email i Vaša lozinka ostat će nepromijenjena."
|
||||
"youAreReceivingResetPassword": "Primili ste ovo jer ste Vi (ili netko drugi) zatražili promjenu lozinke za Vaš račun. Molimo kliknite na poveznicu ili zalijepite ovo u svoje preglednik da biste završili proces:",
|
||||
"youDidNotRequestPassword": "Ako niste zatražili ovo, molimo ignorirajte ovaj e-mail i Vaša će lozinka ostati nepromijenjena."
|
||||
},
|
||||
"error": {
|
||||
"accountAlreadyActivated": "Ovaj račun je već aktiviran.",
|
||||
"autosaving": "Nastao je problem pri automatskom spremanju ovog dokumenta.",
|
||||
"correctInvalidFields": "Molim ispravite nevaljana polja.",
|
||||
"correctInvalidFields": "Molimo ispravite nevaljana polja.",
|
||||
"deletingFile": "Dogodila se pogreška pri brisanju datoteke.",
|
||||
"deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molim provjerite svoju internetsku vezu i pokušajte ponovno.",
|
||||
"emailOrPasswordIncorrect": "Email ili lozinka netočni.",
|
||||
"followingFieldsInvalid_one": " Ovo polje je nevaljano:",
|
||||
"deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molimo provjerite svoju internet vezu i pokušajte ponovno.",
|
||||
"emailOrPasswordIncorrect": "E-mail adresa ili lozinka netočni.",
|
||||
"followingFieldsInvalid_one": "Ovo polje je nevaljano:",
|
||||
"followingFieldsInvalid_other": "Ova polja su nevaljana:",
|
||||
"incorrectCollection": "Nevaljana kolekcija",
|
||||
"invalidFileType": "Nevaljan tip datoteke",
|
||||
@@ -72,7 +72,7 @@
|
||||
"loadingDocument": "Pojavio se problem pri učitavanju dokumenta čiji je ID {{id}}.",
|
||||
"localesNotSaved_one": "Sljedeću lokalnu postavku nije bilo moguće spremiti:",
|
||||
"localesNotSaved_other": "Sljedeće lokalne postavke nije bilo moguće spremiti:",
|
||||
"missingEmail": "Nedostaje email.",
|
||||
"missingEmail": "Nedostaje e-mail.",
|
||||
"missingIDOfDocument": "Nedostaje ID dokumenta da bi se ažurirao.",
|
||||
"missingIDOfVersion": "Nedostaje ID verzije.",
|
||||
"missingRequiredData": "Nedostaju obvezni podaci.",
|
||||
@@ -88,10 +88,10 @@
|
||||
"unPublishingDocument": "Pojavio se problem pri poništavanju objave ovog dokumenta.",
|
||||
"unableToDeleteCount": "Nije moguće izbrisati {{count}} od {{total}} {{label}}.",
|
||||
"unableToUpdateCount": "Nije moguće ažurirati {{count}} od {{total}} {{label}}.",
|
||||
"unauthorized": "Neovlašten, morate biti prijavljeni da biste uputili ovaj zahtjev.",
|
||||
"unauthorized": "Neovlašteno, morate biti prijavljeni da biste uputili ovaj zahtjev.",
|
||||
"unknown": "Došlo je do nepoznate pogreške.",
|
||||
"unspecific": "Došlo je do pogreške.",
|
||||
"userEmailAlreadyRegistered": "Korisnik s navedenom e-poštom je već registriran.",
|
||||
"userEmailAlreadyRegistered": "Korisnik s navedenom e-mail adresom je već registriran.",
|
||||
"userLocked": "Ovaj korisnik je zaključan zbog previše neuspješnih pokušaja prijave.",
|
||||
"valueMustBeUnique": "Vrijednost mora biti jedinstvena.",
|
||||
"verificationTokenInvalid": "Verifikacijski token je nevaljan."
|
||||
@@ -121,18 +121,18 @@
|
||||
"labelRelationship": "{{label}} veza",
|
||||
"latitude": "Zemljopisna širina",
|
||||
"linkType": "Tip poveznce",
|
||||
"linkedTo": "Povezabi sa <0>{{label}}</0>",
|
||||
"linkedTo": "Povezan s <0>{{label}}</0>",
|
||||
"longitude": "Zemljopisna dužina",
|
||||
"newLabel": "Novo {{label}}",
|
||||
"openInNewTab": "Otvori u novoj kartici.",
|
||||
"passwordsDoNotMatch": "Lozinke nisu iste.",
|
||||
"passwordsDoNotMatch": "Lozinke nisu jednake.",
|
||||
"relatedDocument": "Povezani dokument",
|
||||
"relationTo": "Veza sa",
|
||||
"removeRelationship": "Ukloni vezu",
|
||||
"removeUpload": "Ukloni prijenos",
|
||||
"saveChanges": "Spremi promjene",
|
||||
"searchForBlock": "Potraži blok",
|
||||
"selectExistingLabel": "Odaberi postojeće{{label}}",
|
||||
"selectExistingLabel": "Odaberi postojeće {{label}}",
|
||||
"selectFieldsToEdit": "Odaberite polja za uređivanje",
|
||||
"showAll": "Pokaži sve",
|
||||
"swapRelationship": "Zamijeni vezu",
|
||||
@@ -149,7 +149,7 @@
|
||||
"addBelow": "Dodaj ispod",
|
||||
"addFilter": "Dodaj filter",
|
||||
"adminTheme": "Administratorska tema",
|
||||
"and": "I",
|
||||
"and": "i",
|
||||
"applyChanges": "Primijeni promjene",
|
||||
"ascending": "Uzlazno",
|
||||
"automatic": "Automatsko",
|
||||
@@ -177,7 +177,7 @@
|
||||
"dashboard": "Nadzorna ploča",
|
||||
"delete": "Obriši",
|
||||
"deletedCountSuccessfully": "Uspješno izbrisano {{count}} {{label}}.",
|
||||
"deletedSuccessfully": "Uspješno obrisano.",
|
||||
"deletedSuccessfully": "Uspješno izbrisano.",
|
||||
"deleting": "Brisanje...",
|
||||
"depth": "Dubina",
|
||||
"descending": "Silazno",
|
||||
@@ -192,8 +192,8 @@
|
||||
"editingLabel_many": "Uređivanje {{count}} {{label}}",
|
||||
"editingLabel_one": "Uređivanje {{count}} {{label}}",
|
||||
"editingLabel_other": "Uređivanje {{count}} {{label}}",
|
||||
"email": "Email",
|
||||
"emailAddress": "Email adresa",
|
||||
"email": "E-mail",
|
||||
"emailAddress": "E-mail adresa",
|
||||
"enterAValue": "Unesi vrijednost",
|
||||
"error": "Greška",
|
||||
"errors": "Greške",
|
||||
@@ -224,9 +224,9 @@
|
||||
"none": "Nijedan",
|
||||
"notFound": "Nije pronađeno",
|
||||
"nothingFound": "Ništa nije pronađeno",
|
||||
"of": "Od",
|
||||
"of": "od",
|
||||
"open": "Otvori",
|
||||
"or": "Ili",
|
||||
"or": "ili",
|
||||
"order": "Poredak",
|
||||
"pageNotFound": "Stranica nije pronađena",
|
||||
"password": "Lozinka",
|
||||
@@ -284,7 +284,7 @@
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Dodaj datoteku",
|
||||
"crop": "Usjev",
|
||||
"crop": "Izreži",
|
||||
"cropToolDescription": "Povucite kutove odabranog područja, nacrtajte novo područje ili prilagodite vrijednosti ispod.",
|
||||
"dragAndDrop": "Povucite i ispustite datoteku",
|
||||
"dragAndDropHere": "ili povucite i ispustite datoteku ovdje",
|
||||
@@ -307,8 +307,8 @@
|
||||
"width": "Širina"
|
||||
},
|
||||
"validation": {
|
||||
"emailAddress": "Molim unestie valjanu email adresu.",
|
||||
"enterNumber": "Molim unesite valjani broj.",
|
||||
"emailAddress": "Molimo unesite valjanu e-mail adresu.",
|
||||
"enterNumber": "Molimo unesite valjani broj.",
|
||||
"fieldHasNo": "Ovo polje nema {{label}}",
|
||||
"greaterThanMax": "{{value}} exceeds the maximum allowable {{label}} limit of {{max}}.",
|
||||
"invalidInput": "Ovo polje ima nevaljan unos.",
|
||||
@@ -327,12 +327,12 @@
|
||||
"validUploadID": "Ovo polje nije valjani ID prijenosa."
|
||||
},
|
||||
"version": {
|
||||
"aboutToPublishSelection": "Upravo ćete objaviti sve {{label}} u izboru. Jesi li siguran?",
|
||||
"aboutToPublishSelection": "Upravo ćete objaviti sve {{label}} u izboru. Jeste li sigurani?",
|
||||
"aboutToRestore": "Vratit ćete {{label}} dokument u stanje u kojem je bio {{versionDate}}",
|
||||
"aboutToRestoreGlobal": "Vratit ćete globalni {{label}} u stanje u kojem je bio {{versionDate}}.",
|
||||
"aboutToRevertToPublished": "Vratit ćete promjene u dokumentu u objavljeno stanje. Jeste li sigurni? ",
|
||||
"aboutToUnpublish": "Poništit ćete objavu ovog dokumenta. Jeste li sigurni?",
|
||||
"aboutToUnpublishSelection": "Upravo ćete poništiti objavu svih {{label}} u odabiru. Jesi li siguran?",
|
||||
"aboutToUnpublishSelection": "Upravo ćete poništiti objavu svih {{label}} u odabiru. Jeste li sigurni?",
|
||||
"autosave": "Automatsko spremanje",
|
||||
"autosavedSuccessfully": "Automatsko spremanje uspješno.",
|
||||
"autosavedVersion": "Verzija automatski spremljenog dokumenta",
|
||||
@@ -343,8 +343,8 @@
|
||||
"confirmUnpublish": "Potvrdite poništavanje objave",
|
||||
"confirmVersionRestoration": "Potvrdite vraćanje verzije",
|
||||
"currentDocumentStatus": "Trenutni {{docStatus}} dokumenta",
|
||||
"currentDraft": "Trenutačni nacrt",
|
||||
"currentPublishedVersion": "Trenutačno objavljena verzija",
|
||||
"currentDraft": "Trenutni nacrt",
|
||||
"currentPublishedVersion": "Trenutno objavljena verzija",
|
||||
"draft": "Nacrt",
|
||||
"draftSavedSuccessfully": "Nacrt uspješno spremljen.",
|
||||
"lastSavedAgo": "Zadnji put spremljeno prije {{distance}",
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
"noFiltersSet": "Nessun filtro impostato",
|
||||
"noLabel": "<No {{label}}>",
|
||||
"noOptions": "Nessuna opzione",
|
||||
"noResults": "Nessun {{label}} trovato. Non esiste ancora nessun {{label}} oppure nessuno corrisponde ai filtri che hai specificato sopra.",
|
||||
"noResults": "Non abbiamo trovato {{label}}. Potrebbero non esserci {{label}}, oppure nessuno corrisponde ai filtri che hai specificato sopra.",
|
||||
"noValue": "Nessun valore",
|
||||
"none": "Nessuno",
|
||||
"notFound": "Non Trovato",
|
||||
|
||||
@@ -431,15 +431,26 @@ export default async function resizeAndTransformImageSizes({
|
||||
|
||||
const mimeInfo = await fromBuffer(bufferData)
|
||||
|
||||
const imageNameWithDimensions = createImageName({
|
||||
extension: mimeInfo?.ext || sanitizedImage.ext,
|
||||
height: extractHeightFromImage({
|
||||
...originalImageMeta,
|
||||
height: bufferInfo.height,
|
||||
}),
|
||||
outputImageName: sanitizedImage.name,
|
||||
width: bufferInfo.width,
|
||||
})
|
||||
const imageNameWithDimensions = imageResizeConfig.generateImageName
|
||||
? imageResizeConfig.generateImageName({
|
||||
extension: mimeInfo?.ext || sanitizedImage.ext,
|
||||
height: extractHeightFromImage({
|
||||
...originalImageMeta,
|
||||
height: bufferInfo.height,
|
||||
}),
|
||||
originalName: sanitizedImage.name,
|
||||
sizeName: imageResizeConfig.name,
|
||||
width: bufferInfo.width,
|
||||
})
|
||||
: createImageName({
|
||||
extension: mimeInfo?.ext || sanitizedImage.ext,
|
||||
height: extractHeightFromImage({
|
||||
...originalImageMeta,
|
||||
height: bufferInfo.height,
|
||||
}),
|
||||
outputImageName: sanitizedImage.name,
|
||||
width: bufferInfo.width,
|
||||
})
|
||||
|
||||
const imagePath = `${staticPath}/${imageNameWithDimensions}`
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import { mimeTypeValidator } from './mimeTypeValidator'
|
||||
const options = { siblingData: { filename: 'file.xyz' } } as ValidateOptions<
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
undefined,
|
||||
string
|
||||
>
|
||||
|
||||
describe('mimeTypeValidator', () => {
|
||||
|
||||
@@ -51,12 +51,24 @@ export type ImageUploadFormatOptions = {
|
||||
*/
|
||||
export type ImageUploadTrimOptions = Parameters<Sharp['trim']>[0]
|
||||
|
||||
export type GenerateImageName = (args: {
|
||||
extension: string
|
||||
height: number
|
||||
originalName: string
|
||||
sizeName: string
|
||||
width: number
|
||||
}) => string
|
||||
|
||||
export type ImageSize = Omit<ResizeOptions, 'withoutEnlargement'> & {
|
||||
/**
|
||||
* @deprecated prefer position
|
||||
*/
|
||||
crop?: string // comes from sharp package
|
||||
formatOptions?: ImageUploadFormatOptions
|
||||
/**
|
||||
* Generate a custom name for the file of this image size.
|
||||
*/
|
||||
generateImageName?: GenerateImageName
|
||||
name: string
|
||||
trimOptions?: ImageUploadTrimOptions
|
||||
/**
|
||||
@@ -77,6 +89,7 @@ export type IncomingUploadType = {
|
||||
adminThumbnail?: GetAdminThumbnail | string
|
||||
crop?: boolean
|
||||
disableLocalStorage?: boolean
|
||||
displayPreview?: boolean
|
||||
/**
|
||||
* Accepts existing headers and can filter/modify them.
|
||||
*
|
||||
@@ -102,6 +115,7 @@ export type Upload = {
|
||||
adminThumbnail?: GetAdminThumbnail | string
|
||||
crop?: boolean
|
||||
disableLocalStorage?: boolean
|
||||
displayPreview?: boolean
|
||||
filesRequiredOnCreate?: boolean
|
||||
focalPoint?: boolean
|
||||
formatOptions?: ImageUploadFormatOptions
|
||||
|
||||
@@ -23,7 +23,9 @@ export const getLatestCollectionVersion = async <T extends TypeWithID = any>({
|
||||
}: Args): Promise<T> => {
|
||||
let latestVersion: TypeWithVersion<T>
|
||||
|
||||
if (config.versions?.drafts) {
|
||||
const hasConfigDb = Object.keys(config?.db ? config?.db : {}).length > 0
|
||||
|
||||
if (config.versions?.drafts && !hasConfigDb) {
|
||||
const { docs } = await payload.db.findVersions<T>({
|
||||
collection: config.slug,
|
||||
limit: 1,
|
||||
@@ -35,7 +37,12 @@ export const getLatestCollectionVersion = async <T extends TypeWithID = any>({
|
||||
;[latestVersion] = docs
|
||||
}
|
||||
|
||||
const doc = await payload.db.findOne<T>({ ...query, req })
|
||||
let doc
|
||||
if (config?.db?.findOne) {
|
||||
doc = await config.db.findOne<T>({ ...query, req })
|
||||
} else {
|
||||
doc = await payload.db.findOne<T>({ ...query, req })
|
||||
}
|
||||
|
||||
if (!latestVersion || (docHasTimestamps(doc) && latestVersion.updatedAt < doc.updatedAt)) {
|
||||
return doc
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -29,18 +29,27 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
||||
collCacheConfig?.enabled !== false
|
||||
|
||||
return async (req, res, next) => {
|
||||
const filename = req.params.filename
|
||||
let fileKeyWithPrefix = ''
|
||||
|
||||
if (!filename) {
|
||||
req.payload.logger.warn({
|
||||
msg: `No filename provided for static file against collection: ${collection.slug}`,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const { identityID, storageClient } = await getStorageClient()
|
||||
|
||||
const Key = createKey({
|
||||
fileKeyWithPrefix = createKey({
|
||||
collection: collection.slug,
|
||||
filename: req.params.filename,
|
||||
filename,
|
||||
identityID,
|
||||
})
|
||||
|
||||
const object = await storageClient.getObject({
|
||||
Bucket: process.env.PAYLOAD_CLOUD_BUCKET,
|
||||
Key,
|
||||
Key: fileKeyWithPrefix,
|
||||
})
|
||||
|
||||
res.set({
|
||||
@@ -56,7 +65,10 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
||||
|
||||
return next()
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error({ err, msg: 'Error getting file from cloud storage' })
|
||||
req.payload.logger.error({
|
||||
err,
|
||||
msg: `Error getting file from cloud storage: '${fileKeyWithPrefix}'`,
|
||||
})
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.11.2",
|
||||
"version": "0.11.3",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -58,7 +58,7 @@ export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNo
|
||||
nodeValidations: Map<string, Array<NodeValidation>>
|
||||
payloadConfig: SanitizedConfig
|
||||
validation: {
|
||||
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
|
||||
options: ValidateOptions<SerializedEditorState, unknown, RichTextField, SerializedEditorState>
|
||||
value: SerializedEditorState
|
||||
}
|
||||
}) => Promise<string | true> | string | true
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function validateNodes({
|
||||
nodes: SerializedLexicalNode[]
|
||||
payloadConfig: SanitizedConfig
|
||||
validation: {
|
||||
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
|
||||
options: ValidateOptions<SerializedEditorState, unknown, RichTextField, SerializedEditorState>
|
||||
value: SerializedEditorState
|
||||
}
|
||||
}): Promise<string | true> {
|
||||
|
||||
22886
pnpm-lock.yaml
generated
22886
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,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.
|
||||
|
||||
@@ -49,7 +49,7 @@ If you have not done so already, you need to have standalone copy of this repo o
|
||||
1. First [clone the repo](#clone) if you have not done so already
|
||||
1. `cd my-project && cp .env.example .env` to copy the example environment variables
|
||||
1. `yarn && yarn dev` to install dependencies and start the dev server
|
||||
1. `open http://localhost:3000` to open the app in your browser
|
||||
1. Open [http://localhost:3000](http://localhost:3000) to open the app in your browser
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. To begin accepting payment, follow the [Stripe](#stripe) guide. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Header } from '../../../payload/payload-types'
|
||||
import type { Header } from '../../../payload/payload-types'
|
||||
import { fetchHeader } from '../../_api/fetchGlobals'
|
||||
import { Gutter } from '../Gutter'
|
||||
import { HeaderNav } from './Nav'
|
||||
|
||||
@@ -50,7 +50,7 @@ If you have not done so already, you need to have standalone copy of this repo o
|
||||
1. First [clone the repo](#clone) if you have not done so already
|
||||
1. `cd my-project && cp .env.example .env` to copy the example environment variables
|
||||
1. `yarn && yarn dev` to install dependencies and start the dev server
|
||||
1. `open http://localhost:3000` to open the app in your browser
|
||||
1. Open [http://localhost:3000](http://localhost:3000) to open the app in your browser
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user