Compare commits

..

1 Commits

Author SHA1 Message Date
Dan Ribbens
19620f4fa4 chore: enable postgres ci tests 2024-03-27 16:48:15 -04:00
5205 changed files with 217613 additions and 375036 deletions

13
.eslintignore Normal file
View File

@@ -0,0 +1,13 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
playwright.config.ts
jest.config.js
test/live-preview/next-app

64
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,64 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ['@payloadcms'],
ignorePatterns: ['README.md', 'packages/**/*.spec.ts'],
overrides: [
{
files: ['packages/**'],
plugins: ['payload'],
rules: {
'payload/no-jsx-import-statements': 'warn',
},
},
{
files: ['scripts/**'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'no-console': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
},
},
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
{
files: ['packages/eslint-config-payload/**'],
rules: {
'perfectionist/sort-objects': 'off',
},
},
{
files: ['package.json', 'tsconfig.json'],
rules: {
'perfectionist/sort-array-includes': 'off',
'perfectionist/sort-astro-attributes': 'off',
'perfectionist/sort-classes': 'off',
'perfectionist/sort-enums': 'off',
'perfectionist/sort-exports': 'off',
'perfectionist/sort-imports': 'off',
'perfectionist/sort-interfaces': 'off',
'perfectionist/sort-jsx-props': 'off',
'perfectionist/sort-keys': 'off',
'perfectionist/sort-maps': 'off',
'perfectionist/sort-named-exports': 'off',
'perfectionist/sort-named-imports': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
'perfectionist/sort-svelte-attributes': 'off',
'perfectionist/sort-union-types': 'off',
'perfectionist/sort-vue-attributes': 'off',
},
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
EXPERIMENTAL_useProjectService: true,
sourceType: 'module',
ecmaVersion: 'latest',
},
root: true,
}

View File

@@ -19,12 +19,3 @@ fb7d1be2f3325d076b7c967b1730afcef37922c2
# 3.0 prettier & lint everywhere
6789e61488a1d3de56f472ac3214faf344030005
# 3.0 prettier & lint everywhere again
83fd4c66222d7846eeb5cc332dfa99bf1e830831
# Upgrade to typescript-eslint v8, then prettier & lint everywhere
86fdad0bb8ab27810599c8a32f3d8cba1341e1df
# Prettier and lint remaining db packages
7fd736ea5b2e9fc4ef936e9dc9e5e3d722f6d8bf

48
.github/CODEOWNERS vendored
View File

@@ -1,29 +1,41 @@
# Order matters. The last matching pattern takes precedence.
# Approvals are not required currently but may be enabled in the future.
### Package Exports ###
/**/exports/ @denolfe @jmikrut @DanRibbens
### Core ###
/packages/payload/src/uploads/ @denolfe
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
### Packages ###
/packages/plugin-cloud*/src/ @denolfe
/packages/email-*/src/ @denolfe
/packages/storage-*/src/ @denolfe
/packages/create-payload-app/src/ @denolfe
/packages/eslint-*/ @denolfe @AlessioGr
### Adapters ###
/packages/db-*/ @denolfe @jmikrut @DanRibbens
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Plugins ###
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens
/packages/plugin-cloud*/ @denolfe
/packages/plugin-form-builder/ @jacobsfletch
/packages/plugin-live-preview*/ @jacobsfletch
/packages/plugin-nested-docs/ @jacobsfletch
/packages/plugin-redirects/ @jacobsfletch
/packages/plugin-search/ @jacobsfletch
/packages/plugin-sentry/ @JessChowdhury
/packages/plugin-seo/ @jacobsfletch
/packages/plugin-stripe/ @jacobsfletch
### Examples ###
/examples/ @jacobsfletch
/examples/testing/ @JarrodMFlesch
/examples/email/ @JessChowdhury
/examples/whitelabel/ @JessChowdhury
### Templates ###
/templates/_data/ @denolfe
/templates/_template/ @denolfe
/templates/ @jacobsfletch @denolfe
### Build Files ###
/tsconfig.json @denolfe
/**/tsconfig*.json @denolfe
/jest.config.js @denolfe
/**/jest.config.js @denolfe
### Misc ###
/packages/create-payload-app/ @denolfe
/packages/eslint-config-payload/ @denolfe
/packages/payload-admin-bar/ @jacobsfletch
### Root ###
/package.json @denolfe
/scripts/ @denolfe
/.husky/ @denolfe
/.vscode/ @denolfe @AlessioGr
/.github/ @denolfe
/.github/CODEOWNERS @denolfe

View File

@@ -1,13 +0,0 @@
module.exports = {
env: {
es6: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
}

View File

@@ -1,74 +0,0 @@
# Release Commenter
This GitHub Action automatically comments on and/or labels Issues and PRs when a fix is released for them.
> [!IMPORTANT]
> 🔧 Heavily modified version of https://github.com/apexskier/github-release-commenter
## Fork Modifications
- Filters to closed PRs only
- Adds tag filter to support non-linear releases
- Better logging
- Moved to pnpm
- Uses @vercel/ncc for packaging
- Comments on locked issues by unlocking then re-locking
## How it works
Use this action in a workflow [triggered by a release](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#release). It will scan commits between that and the prior release, find associated Issues and PRs, and comment on them to let people know a release has been made. Associated Issues and PRs can be directly [linked](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) to the commit or manually linked from a PR associated with the commit.
## Inputs
**GITHUB_TOKEN**
A GitHub personal access token with repo scope, such as [`secrets.GITHUB_TOKEN`](https://docs.github.com/en/free-pro-team@latest/actions/reference/authentication-in-a-workflow#about-the-github_token-secret).
**comment-template** (optional)
Override the comment posted on Issues and PRs. Set to the empty string to disable commenting. Several variables strings will be automatically replaced:
- `{release_link}` - a markdown link to the release
- `{release_name}` - the release's name
- `{release_tag}` - the release's tag
**label-template** (optional)
Add the given label. Multiple labels can be separated by commas. Several variable strings will be automatically replaced:
- `{release_name}` - the release's name
- `{release_tag}` - the release's tag
**skip-label** (optional)
Skip processing if any of the given labels are present. Same processing rules as **label-template**. Default is "dependencies".
## Example
```yml
on:
release:
types: [published]
jobs:
release:
steps:
- uses: apexskier/github-release-commenter@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
comment-template: |
Release {release_link} addresses this.
```
## Known limitations
These are some known limitations of this action. I'd like to try to address them in the future.
- Non-linear releases aren't supported. For example, releasing a patch to a prior major release after a new major release has been bumped.
- Non-sequential releases aren't supported. For example, if you release multiple prereleases between two official releases, this will only create a comment for the first prerelease in which a fix is released, not the final release.
- The first release for a project will be ignored. This is intentional, as the use case is unlikely. Most projects will either have several alphas that don't need release comments, or won't use issues/PRs for the first commit.
- If a large number of things are commented on, you may see the error `Error: You have triggered an abuse detection mechanism. Please wait a few minutes before you try again.`. Consider using the `skip-label` input to reduce your load on the GitHub API.
## Versions
Workflows will automatically update the tags `v1` and `latest`, allowing you to reference one of those instead of locking to a specific release.

View File

@@ -1,32 +0,0 @@
name: Release Commenter
description: Comment on PRs and Issues when a fix is released
branding:
icon: message-square
color: blue
inputs:
GITHUB_TOKEN:
description: |
A GitHub personal access token with repo scope, such as
secrets.GITHUB_TOKEN.
required: true
comment-template:
description: |
Text template for the comment string.
required: false
default: |
Included in release {release_link}
label-template:
description: Add the given label. Multiple labels can be separated by commas.
required: false
skip-label:
description: Skip commenting if any of the given label are present. Multiple labels can be separated by commas.
required: false
default: 'dependencies'
tag-filter:
description: |
Filter tags by a regular expression. Must be escaped. e.g. 'v\\d' to isolate tags between major versions.
required: false
default: null
runs:
using: node20
main: dist/index.js

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
module.exports = {
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
}

View File

@@ -1,34 +0,0 @@
{
"name": "release-commenter",
"version": "0.0.0",
"private": true,
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
"license": "MIT",
"main": "dist/index.js",
"scripts": {
"build": "pnpm build:typecheck && pnpm build:ncc",
"build:ncc": "ncc build src/index.ts -t -o dist",
"build:typecheck": "tsc",
"clean": "rimraf dist",
"test": "jest"
},
"dependencies": {
"@actions/core": "^1.3.0",
"@actions/github": "^5.0.0"
},
"devDependencies": {
"@octokit/webhooks-types": "^7.5.1",
"@swc/jest": "^0.2.36",
"@types/jest": "^27.5.2",
"@types/node": "^20.16.5",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vercel/ncc": "0.38.1",
"concurrently": "^8.2.2",
"eslint": "^7.32.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"ts-jest": "^26.5.6",
"typescript": "^4.9.5"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,266 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tests feature tests can apply labels 1`] = `
[
[
{
"issue_number": 123,
"labels": [
":dart: landed",
"release-current_tag_name",
"Release Name",
],
},
],
[
{
"issue_number": 7,
"labels": [
":dart: landed",
"release-current_tag_name",
"Release Name",
],
},
],
]
`;
exports[`tests main test 1`] = `
{
"graphql": [MockFunction] {
"calls": [
[
"
{
resource(url: "http://repository/commit/SHA1") {
... on Commit {
messageHeadlineHTML
messageBodyHTML
associatedPullRequests(first: 10) {
pageInfo {
hasNextPage
}
edges {
node {
bodyHTML
number
state
labels(first: 10) {
pageInfo {
hasNextPage
}
nodes {
name
}
}
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
pageInfo {
hasNextPage
}
nodes {
... on ConnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
... on DisconnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
}
}
}
}
}
}
}
}
",
],
[
"
{
resource(url: "http://repository/commit/SHA2") {
... on Commit {
messageHeadlineHTML
messageBodyHTML
associatedPullRequests(first: 10) {
pageInfo {
hasNextPage
}
edges {
node {
bodyHTML
number
state
labels(first: 10) {
pageInfo {
hasNextPage
}
nodes {
name
}
}
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
pageInfo {
hasNextPage
}
nodes {
... on ConnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
... on DisconnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
}
}
}
}
}
}
}
}
",
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
],
},
"rest": {
"issues": {
"addLabels": [MockFunction],
"createComment": [MockFunction] {
"calls": [
[
{
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
"issue_number": 3,
},
],
[
{
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
"issue_number": 123,
},
],
[
{
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
"issue_number": 7,
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
],
},
"get": [MockFunction] {
"calls": [
[
{
"issue_number": 3,
},
],
[
{
"issue_number": 123,
},
],
[
{
"issue_number": 7,
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
],
},
},
"repos": {
"compareCommits": [MockFunction] {
"calls": [
[
{
"base": "prior_tag_name",
"head": "current_tag_name",
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
],
},
"listReleases": [MockFunction] {
"calls": [
[
{
"per_page": 100,
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
],
},
},
},
}
`;

View File

@@ -1,399 +0,0 @@
import type * as githubModule from '@actions/github'
import type * as coreModule from '@actions/core'
import { mock } from 'node:test'
jest.mock('@actions/core')
jest.mock('@actions/github')
type Mocked<T> = {
-readonly [P in keyof T]: T[P] extends Function ? jest.Mock<T[P]> : jest.Mocked<Partial<T[P]>>
}
const github = require('@actions/github') as jest.Mocked<Mocked<typeof githubModule>>
const core = require('@actions/core') as jest.Mocked<Mocked<typeof coreModule>>
describe('tests', () => {
let mockOctokit: any = {}
let currentTag: string = 'current_tag_name'
;(core.warning as any) = jest.fn(console.warn.bind(console))
;(core.error as any) = jest.fn(console.error.bind(console))
let commentTempate: string = ''
let labelTemplate: string | null = null
const skipLabelTemplate: string | null = 'skip,test'
let tagFilter: string | RegExp | null = null
let simpleMockOctokit: any = {}
beforeEach(() => {
tagFilter = null
currentTag = 'current_tag_name'
;(github.context as any) = {
payload: {
repo: {
owner: 'owner',
repo: 'repo',
},
release: {
tag_name: currentTag,
},
repository: { html_url: 'http://repository' },
},
}
github.getOctokit.mockReset().mockImplementationOnce(((token: string) => {
expect(token).toBe('GITHUB_TOKEN_VALUE')
return mockOctokit
}) as any)
;(core.getInput as any).mockImplementation((key: string) => {
if (key == 'GITHUB_TOKEN') {
return 'GITHUB_TOKEN_VALUE'
}
if (key == 'comment-template') {
return commentTempate
}
if (key == 'label-template') {
return labelTemplate
}
if (key == 'skip-label') {
return skipLabelTemplate
}
if (key == 'tag-filter') {
return tagFilter
}
fail(`Unexpected input key ${key}`)
})
commentTempate =
'Included in release {release_link}. Replacements: {release_name}, {release_tag}.'
labelTemplate = null
simpleMockOctokit = {
rest: {
issues: {
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
createComment: jest.fn(() => Promise.resolve()),
addLabels: jest.fn(() => Promise.resolve()),
},
repos: {
listReleases: jest.fn(() =>
Promise.resolve({
data: [
{
name: 'Release Name',
tag_name: 'current_tag_name',
html_url: 'http://current_release',
},
{
tag_name: 'prior_tag_name',
html_url: 'http://prior_release',
},
],
}),
),
compareCommits: jest.fn(() =>
Promise.resolve({
data: { commits: [{ sha: 'SHA1' }] },
}),
),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML: '',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [],
},
},
}),
),
}
})
afterEach(() => {
expect(core.error).not.toHaveBeenCalled()
expect(core.warning).not.toHaveBeenCalled()
expect(core.setFailed).not.toHaveBeenCalled()
})
test('main test', async () => {
mockOctokit = {
...simpleMockOctokit,
rest: {
issues: {
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
createComment: jest.fn(() => Promise.resolve()),
addLabels: jest.fn(() => Promise.resolve()),
},
repos: {
listReleases: jest.fn(() =>
Promise.resolve({
data: [
{
tag_name: 'current_tag_name',
html_url: 'http://current_release',
},
{
tag_name: 'prior_tag_name',
html_url: 'http://prior_release',
},
],
}),
),
compareCommits: jest.fn(() =>
Promise.resolve({
data: { commits: [{ sha: 'SHA1' }, { sha: 'SHA2' }] },
}),
),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #3.">Closes</span> <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="718013420" data-permission-text="Title is private" data-url="https://github.com/apexskier/github-release-commenter/issues/1" data-hovercard-type="issue" data-hovercard-url="/apexskier/github-release-commenter/issues/1/hovercard" href="https://github.com/apexskier/github-release-commenter/issues/1">#1</a>',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [
{
node: {
bodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #4.">Closes</span> <span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #5.">Closes</span>',
number: 9,
labels: {
pageInfo: { hasNextPage: false },
nodes: [{ name: 'label1' }, { name: 'label2' }],
},
timelineItems: {
pageInfo: { hasNextPage: false },
nodes: [
{
isCrossRepository: true,
__typename: 'ConnectedEvent',
subject: { number: 1 },
},
{
isCrossRepository: false,
__typename: 'ConnectedEvent',
subject: { number: 2 },
},
{
isCrossRepository: false,
__typename: 'DisconnectedEvent',
subject: { number: 2 },
},
{
isCrossRepository: false,
__typename: 'ConnectedEvent',
subject: { number: 2 },
},
],
},
},
},
{
node: {
bodyHTML: '',
number: 42,
labels: {
pageInfo: { hasNextPage: false },
nodes: [{ name: 'label1' }, { name: 'skip' }],
},
timelineItems: {
pageInfo: { hasNextPage: false },
nodes: [
{
isCrossRepository: true,
__typename: 'ConnectedEvent',
subject: { number: 82 },
},
],
},
},
},
],
},
},
}),
),
}
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>(setImmediate)
expect(mockOctokit).toMatchSnapshot()
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(3)
})
describe('can filter tags', () => {
const v3prev = 'v3.0.1'
const v3current = 'v3.0.2'
const v2prev = 'v2.0.1'
const v2current = 'v2.0.2'
const listReleasesData = [
{
name: 'Current Release Name',
tag_name: v3current,
html_url: 'http://v3.0.2',
},
{
name: 'Prev Release Name',
tag_name: v3prev,
html_url: 'http://v3.0.1',
},
{
name: 'v2 Current Release Name',
tag_name: v2current,
html_url: 'http://v2.0.2',
},
{
name: 'v2 Prev Release Name',
tag_name: v2prev,
html_url: 'http://v2.0.1',
},
]
it.each`
description | prevTag | currentTag | filter
${'no filter'} | ${v3prev} | ${v3current} | ${null}
${'v3'} | ${v3prev} | ${v3current} | ${'v\\d'}
${'v2'} | ${v2prev} | ${v2current} | ${'v\\d'}
`('should filter tags with $description', async ({ prevTag, currentTag, filter }) => {
// @ts-ignore
github.context.payload.release.tag_name = currentTag
tagFilter = filter
mockOctokit = {
...simpleMockOctokit,
rest: {
issues: {
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
createComment: jest.fn(() => Promise.resolve()),
addLabels: jest.fn(() => Promise.resolve()),
},
repos: {
listReleases: jest.fn(() =>
Promise.resolve({
data: listReleasesData,
}),
),
compareCommits: jest.fn(() =>
Promise.resolve({
data: { commits: [{ sha: 'SHA1' }] },
}),
),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML: '',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [],
},
},
}),
),
}
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
expect(mockOctokit.rest.repos.compareCommits.mock.calls).toEqual([
[{ base: prevTag, head: currentTag }],
])
})
})
describe('feature tests', () => {
beforeEach(() => {
mockOctokit = simpleMockOctokit
})
it('can disable comments', async () => {
commentTempate = ''
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
})
it('should unlock and comment', async () => {
mockOctokit = {
...simpleMockOctokit,
rest: {
...simpleMockOctokit.rest,
issues: {
// Return locked for both issues to be commented on
get: jest.fn(() => Promise.resolve({ data: { locked: true } })),
lock: jest.fn(() => Promise.resolve()),
unlock: jest.fn(() => Promise.resolve()),
createComment: jest.fn(() => Promise.resolve()),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML: '',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [],
},
},
}),
),
}
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
// Should call once for both linked issues
expect(mockOctokit.rest.issues.unlock).toHaveBeenCalledTimes(2)
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(2)
expect(mockOctokit.rest.issues.lock).toHaveBeenCalledTimes(2)
})
it.skip('can apply labels', async () => {
labelTemplate = ':dart: landed,release-{release_tag},{release_name}'
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
expect(mockOctokit.rest.issues.addLabels.mock.calls).toMatchSnapshot()
})
})
})

View File

@@ -1,349 +0,0 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import type * as Webhooks from '@octokit/webhooks-types'
const closesMatcher = /aria-label="This (?:commit|pull request) closes issue #(\d+)\."/g
const releaseLinkTemplateRegex = /{release_link}/g
const releaseNameTemplateRegex = /{release_name}/g
const releaseTagTemplateRegex = /{release_tag}/g
;(async function main() {
try {
const payload = github.context.payload as Webhooks.EventPayloadMap['release']
const githubToken = core.getInput('GITHUB_TOKEN')
const tagFilter = core.getInput('tag-filter') || undefined // Accept tag filter as an input
const octokit = github.getOctokit(githubToken)
const commentTemplate = core.getInput('comment-template')
const labelTemplate = core.getInput('label-template') || null
const skipLabelTemplate = core.getInput('skip-label') || null
// Fetch the releases with the optional tag filter applied
const { data: rawReleases } = await octokit.rest.repos.listReleases({
...github.context.repo,
per_page: 100,
})
// Get the current release tag or latest tag
const currentTag = payload?.release?.tag_name || rawReleases?.[0]?.tag_name
let releases = rawReleases
// Filter releases by the tag filter if provided
if (tagFilter) {
core.info(`Filtering releases by tag filter: ${tagFilter}`)
// Get the matching part of the current release tag
const regexMatch = currentTag.match(tagFilter)?.[0]
if (!regexMatch) {
core.error(`Current release tag ${currentTag} does not match the tag filter ${tagFilter}`)
return
}
core.info(`Matched string from filter: ${regexMatch}`)
releases = releases
.filter((release) => {
const match = release.tag_name.match(regexMatch)?.[0]
return match
})
.slice(0, 2)
}
core.info(`Releases: ${JSON.stringify(releases, null, 2)}`)
if (releases.length < 2) {
if (!releases.length) {
core.error(`No releases found with the provided tag filter: '${tagFilter}'`)
return
}
core.info('first release')
return
}
const [currentRelease, priorRelease] = releases
core.info(`${priorRelease.tag_name}...${currentRelease.tag_name}`)
const {
data: { commits },
} = await octokit.rest.repos.compareCommits({
...github.context.repo,
base: priorRelease.tag_name,
head: currentRelease.tag_name,
})
if (!currentRelease.name) {
core.info('Current release has no name, will fall back to the tag name.')
}
const releaseLabel = currentRelease.name || currentRelease.tag_name
const comment = commentTemplate
.trim()
.split(releaseLinkTemplateRegex)
.join(`[${releaseLabel}](${currentRelease.html_url})`)
.split(releaseNameTemplateRegex)
.join(releaseLabel)
.split(releaseTagTemplateRegex)
.join(currentRelease.tag_name)
const parseLabels = (rawInput: string | null) =>
rawInput
?.split(releaseNameTemplateRegex)
.join(releaseLabel)
?.split(releaseTagTemplateRegex)
.join(currentRelease.tag_name)
?.split(',')
?.map((l) => l.trim())
.filter((l) => l)
const labels = parseLabels(labelTemplate)
const skipLabels = parseLabels(skipLabelTemplate)
const linkedIssuesPrs = new Set<number>()
await Promise.all(
commits.map((commit) =>
(async () => {
const query = `
{
resource(url: "${payload.repository.html_url}/commit/${commit.sha}") {
... on Commit {
messageHeadlineHTML
messageBodyHTML
associatedPullRequests(first: 10) {
pageInfo {
hasNextPage
}
edges {
node {
bodyHTML
number
state
labels(first: 10) {
pageInfo {
hasNextPage
}
nodes {
name
}
}
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
pageInfo {
hasNextPage
}
nodes {
... on ConnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
... on DisconnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
}
}
}
}
}
}
}
}
`
const response: {
resource: null | {
messageHeadlineHTML: string
messageBodyHTML: string
associatedPullRequests: {
pageInfo: { hasNextPage: boolean }
edges: ReadonlyArray<{
node: {
bodyHTML: string
number: number
state: 'OPEN' | 'CLOSED' | 'MERGED'
labels: {
pageInfo: { hasNextPage: boolean }
nodes: ReadonlyArray<{
name: string
}>
}
timelineItems: {
pageInfo: { hasNextPage: boolean }
nodes: ReadonlyArray<{
__typename: 'ConnectedEvent' | 'DisconnectedEvent'
isCrossRepository: boolean
subject: {
number: number
}
}>
}
}
}>
}
}
} = await octokit.graphql(query)
if (!response.resource) {
return
}
// core.info(JSON.stringify(response.resource, null, 2))
core.info(`Checking commit: ${payload.repository.html_url}/commit/${commit.sha}`)
const associatedClosedPREdges = response.resource.associatedPullRequests.edges.filter(
(e) => e.node.state === 'MERGED',
)
if (associatedClosedPREdges.length) {
core.info(
` Associated Merged PRs:\n ${associatedClosedPREdges.map((pr) => `${payload.repository.html_url}/pull/${pr.node.number}`).join('\n ')}`,
)
} else {
core.info(' No associated merged PRs')
}
const html = [
response.resource.messageHeadlineHTML,
response.resource.messageBodyHTML,
...associatedClosedPREdges.map((pr) => pr.node.bodyHTML),
].join(' ')
for (const match of html.matchAll(closesMatcher)) {
const [, num] = match
linkedIssuesPrs.add(parseInt(num, 10))
core.info(
` Linked issue/PR from closesMatcher: ${payload.repository.html_url}/pull/${num}`,
)
}
if (response.resource.associatedPullRequests.pageInfo.hasNextPage) {
core.warning(`Too many PRs associated with ${commit.sha}`)
}
const seen = new Set<number>()
for (const associatedPR of associatedClosedPREdges) {
if (associatedPR.node.timelineItems.pageInfo.hasNextPage) {
core.warning(`Too many links for #${associatedPR.node.number}`)
}
if (associatedPR.node.labels.pageInfo.hasNextPage) {
core.warning(`Too many labels for #${associatedPR.node.number}`)
}
// a skip labels is present on this PR
if (
skipLabels?.some((l) => associatedPR.node.labels.nodes.some(({ name }) => name === l))
) {
continue
}
linkedIssuesPrs.add(associatedPR.node.number)
core.info(
` Linked issue/PR from associated PR: ${payload.repository.html_url}/pull/${associatedPR.node.number}`,
)
// These are sorted by creation date in ascending order. The latest event for a given issue/PR is all we need
// ignore links that aren't part of this repo
const links = associatedPR.node.timelineItems.nodes
.filter((node) => !node.isCrossRepository)
.reverse()
for (const link of links) {
if (seen.has(link.subject.number)) {
continue
}
if (link.__typename == 'ConnectedEvent') {
linkedIssuesPrs.add(link.subject.number)
core.info(
`Linked issue/PR from connected event: ${payload.repository.html_url}/pull/${link.subject.number}`,
)
}
seen.add(link.subject.number)
}
}
})(),
),
)
core.info(
`Final issues/PRs to be commented on: \n${Array.from(linkedIssuesPrs)
.map((num) => ` ${payload.repository.html_url}/pull/${num}`)
.join('\n')}`,
)
const requests: Array<Promise<unknown>> = []
for (const issueNumber of linkedIssuesPrs) {
const baseRequest = {
...github.context.repo,
issue_number: issueNumber,
}
if (comment) {
const commentRequest = {
...baseRequest,
body: comment,
}
// Check if issue is locked or not
const { data: issue } = await octokit.rest.issues.get(baseRequest)
let createCommentPromise: () => Promise<void>
if (!issue.locked) {
createCommentPromise = async () => {
try {
await octokit.rest.issues.createComment(commentRequest)
} catch (error) {
core.error(error as Error)
core.error(
`Failed to comment on issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
)
}
}
} else {
core.info(
`Issue/PR is locked: ${issueNumber}. Unlocking, commenting, and re-locking. ${payload.repository.html_url}/pull/${issueNumber}`,
)
createCommentPromise = async () => {
try {
core.debug(`Unlocking issue/PR: ${issueNumber}`)
await octokit.rest.issues.unlock(baseRequest)
core.debug(`Commenting on issue/PR: ${issueNumber}`)
await octokit.rest.issues.createComment(commentRequest)
core.debug(`Re-locking issue/PR: ${issueNumber}`)
await octokit.rest.issues.lock(baseRequest)
} catch (error) {
core.error(error as Error)
core.error(
`Failed to unlock, comment, and re-lock issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
)
}
}
}
requests.push(createCommentPromise())
}
if (labels) {
const request = {
...baseRequest,
labels,
}
// core.info(JSON.stringify(request, null, 2))
requests.push(octokit.rest.issues.addLabels(request))
}
}
await Promise.all(requests)
} catch (error) {
core.error(error as Error)
core.setFailed((error as Error).message)
}
})()

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es2020.string"],
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"downlevelIteration": true,
"skipLibCheck": true,
},
"exclude": ["src/**/*.test.ts"]
}

3927
.github/pnpm-lock.yaml generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
packages:
- 'actions/*'

View File

@@ -1,50 +0,0 @@
name: label-author
on:
pull_request:
types: [opened]
issues:
types: [opened]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
debug-context:
runs-on: ubuntu-latest
steps:
- name: View context attributes
uses: actions/github-script@v7
with:
script: console.log(context)
label-created-by:
name: Label pr/issue on opening
runs-on: ubuntu-latest
steps:
- name: Tag with 'created-by'
uses: actions/github-script@v7
if: github.event.action == 'opened'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const type = context.payload.pull_request ? 'pull_request' : 'issue';
const association = context.payload[type].author_association;
let label = ''
if (association === 'MEMBER' || association === 'OWNER') {
label = 'created-by: Payload team';
} else if (association === 'CONTRIBUTOR') {
label = 'created-by: Contributor';
}
if (!label) return;
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [label],
});
console.log('Added created-by: Payload team label');

View File

@@ -2,25 +2,9 @@ name: build
on:
pull_request:
types:
- opened
- reopened
- synchronize
types: [opened, reopened, synchronize]
push:
branches:
- main
- beta
concurrency:
# <workflow_name>-<branch_name>-<true || commit_sha if branch is protected>
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref_protected && github.sha || ''}}
cancel-in-progress: true
env:
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
branches: ['main', 'alpha']
jobs:
changes:
@@ -31,10 +15,6 @@ jobs:
needs_build: ${{ steps.filter.outputs.needs_build }}
templates: ${{ steps.filter.outputs.templates }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
with:
fetch-depth: 25
@@ -55,52 +35,7 @@ jobs:
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
echo "templates: ${{ steps.filter.outputs.templates }}"
lint:
if: >
github.event_name == 'pull_request' && !contains(github.event.pull_request.title, 'no-lint') && !contains(github.event.pull_request.title, 'skip-lint')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
timeout-minutes: 720
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- name: Lint staged
run: |
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
npx lint-staged --diff="origin/${GITHUB_BASE_REF}...${GITHUB_SHA}"
build:
core-build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
@@ -110,19 +45,15 @@ jobs:
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Get pnpm store directory
@@ -130,63 +61,65 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
timeout-minutes: 720
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- run: pnpm run build:all
env:
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
- run: pnpm run build:core
- name: Cache build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
tests-unit:
plugins-build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
needs: build
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
with:
fetch-depth: 25
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Unit Tests
run: pnpm test:unit
env:
NODE_OPTIONS: --max-old-space-size=8096
- run: pnpm install
- run: pnpm run build:plugins
tests-int:
tests:
runs-on: ubuntu-latest
needs: build
needs: core-build
strategy:
fail-fast: false
matrix:
@@ -196,7 +129,6 @@ jobs:
- postgres-custom-schema
- postgres-uuid
- supabase
- sqlite
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -207,25 +139,22 @@ jobs:
AWS_REGION: us-east-1
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- run: pnpm install
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
@@ -273,7 +202,7 @@ jobs:
if: matrix.database == 'supabase'
- name: Integration Tests
run: pnpm test:int
run: pnpm test:int --testPathIgnorePatterns=test/fields # Ignore fields tests until reworked
env:
NODE_OPTIONS: --max-old-space-size=8096
PAYLOAD_DATABASE: ${{ matrix.database }}
@@ -281,7 +210,7 @@ jobs:
tests-e2e:
runs-on: ubuntu-latest
needs: build
needs: core-build
strategy:
fail-fast: false
matrix:
@@ -289,174 +218,75 @@ jobs:
suite:
- _community
- access-control
- admin__e2e__1
- admin__e2e__2
- admin-root
# - admin
- auth
- field-error-states
- fields-relationship
- fields
- fields__collections__Blocks
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Date
- fields__collections__Number
- fields__collections__Point
- fields__collections__Tabs
- fields__collections__Text
- fields__collections__Upload
# - field-error-states
# - fields-relationship
# - fields
- fields/lexical
- live-preview
- localization
- i18n
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs
- plugin-seo
- versions
- uploads
env:
SUITE_NAME: ${{ matrix.suite }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
# - localization
# - plugin-nested-docs
# - plugin-seo
# - refresh-permissions
# - uploads
# - versions
- name: Setup Node@${{ env.NODE_VERSION }}
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
- name: Store Playwright's Version
run: |
# Extract the version number using a more targeted regex pattern with awk
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers for Playwright's Version
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Setup Playwright - Browsers and Dependencies
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Setup Playwright - Dependencies-only
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- name: Install Playwright
run: pnpm exec playwright install
- name: E2E Tests
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e:prod:ci ${{ matrix.suite }}
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
NEXT_TELEMETRY_DISABLED: 1
uses: nick-fields/retry@v3
with:
retry_on: error
max_attempts: 2
timeout_minutes: 15
command: pnpm test:e2e ${{ matrix.suite }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.suite }}
name: test-results
path: test/test-results/
if-no-files-found: ignore
retention-days: 1
# Disabled until this is fixed: https://github.com/daun/playwright-report-summary/issues/156
# - uses: daun/playwright-report-summary@v3
# with:
# report-file: results_${{ matrix.suite }}.json
# report-tag: ${{ matrix.suite }}
# job-summary: true
app-build-with-packed:
if: false # Disable until package resolution in tgzs can be figured out
runs-on: ubuntu-latest
needs: build
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: 6.0
- name: Pack and build app
run: |
set -ex
pnpm run script:pack --dest templates/blank
cd templates/blank
cp .env.example .env
ls -la
pnpm add ./*.tgz --ignore-workspace
pnpm install --ignore-workspace --no-frozen-lockfile
cat package.json
pnpm run build
tests-type-generation:
if: false # This should be replaced with gen on a real Payload project
runs-on: ubuntu-latest
needs: build
needs: core-build
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -467,6 +297,46 @@ jobs:
- name: Generate GraphQL schema file
run: pnpm dev:generate-graphql-schema graphql-schema-gen
plugins:
runs-on: ubuntu-latest
needs: core-build
strategy:
fail-fast: false
matrix:
pkg:
- create-payload-app
- plugin-cloud
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs
- plugin-search
- plugin-sentry
- plugin-seo
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build ${{ matrix.pkg }}
run: pnpm turbo run build --filter=${{ matrix.pkg }}
- name: Test ${{ matrix.pkg }}
run: pnpm --filter ${{ matrix.pkg }} run test
templates:
needs: changes
if: false # Disable until templates are updated for 3.0
@@ -480,17 +350,14 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: 18
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.11.0
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: 6.0
@@ -501,62 +368,3 @@ jobs:
yarn install
yarn build
yarn generate:types
generated-templates:
needs: build
if: false # Needs to pull in tgz files from build
runs-on: ubuntu-latest
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build all generated templates
run: pnpm tsx ./scripts/build-generated-templates.ts
all-green:
name: All Green
if: always()
runs-on: ubuntu-latest
needs:
- lint
- build
- tests-unit
- tests-int
- tests-e2e
steps:
- if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
run: exit 1
publish-canary:
name: Publish Canary
runs-on: ubuntu-latest
needs:
- all-green
steps:
# debug github.ref output
- run: |
echo github.ref: ${{ github.ref }}
echo isBeta: ${{ github.ref == 'refs/heads/beta' }}
echo isMain: ${{ github.ref == 'refs/heads/main' }}

View File

@@ -1,31 +0,0 @@
name: post-release
on:
release:
types:
- published
workflow_dispatch:
jobs:
post_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Only needed if debugging on a branch other than default
# ref: ${{ github.event.release.target_commitish || github.ref }}
- 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}

View File

@@ -1,121 +0,0 @@
name: pr-title
on:
pull_request:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: write
jobs:
main:
name: lint-pr-title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
build
chore
ci
docs
feat
fix
perf
refactor
revert
style
test
types
scopes: |
cpa
db-\*
db-mongodb
db-postgres
db-sqlite
drizzle
email-nodemailer
eslint
graphql
live-preview
live-preview-react
next
payload
plugin-cloud
plugin-cloud-storage
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-relationship-object-ids
plugin-search
plugin-sentry
plugin-seo
plugin-stripe
richtext-\*
richtext-lexical
richtext-slate
storage-\*
storage-azure
storage-gcs
storage-vercel-blob
storage-s3
translations
ui
templates
examples(\/(\w|-)+)?
deps
# Disallow uppercase letters at the beginning of the subject
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Pull Request titles must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and have valid scopes.
${{ steps.lint_pr_title.outputs.error_message }}
```
feat(ui): add Button component
^ ^ ^
| | |__ Subject
| |_______ Scope
|____________ Type
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true
label-pr-on-open:
name: label-pr-on-open
runs-on: ubuntu-latest
if: github.event.action == 'opened'
steps:
- name: Tag with main branch with v2
if: github.event.pull_request.base.ref == 'main'
uses: actions-ecosystem/action-add-labels@v1
with:
labels: v2
- name: Tag with beta branch with v3
if: github.event.pull_request.base.ref == 'beta'
uses: actions-ecosystem/action-add-labels@v1
with:
labels: v3

View File

@@ -1,36 +0,0 @@
name: release-canary
on:
workflow_dispatch:
branches:
- beta
env:
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
release:
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Load npm token
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Canary release script
# dry run hard-coded to true for testing and no npm token provided
run: pnpm tsx ./scripts/publish-canary.ts
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true

27
.gitignore vendored
View File

@@ -3,12 +3,7 @@ package-lock.json
dist
/.idea/*
!/.idea/runConfigurations
!/.idea/payload.iml
# Custom actions
!.github/actions/**/dist
test/packed
test-results
.devcontainer
.localstack
@@ -16,20 +11,10 @@ test-results
.localstack
.turbo
meta_client.json
meta_server.json
meta_index.json
meta_shared.json
.turbo
# Ignore test directory media folder/files
/media
test/media
*payloadtests.db
*payloadtests.db-journal
*payloadtests.db-shm
*payloadtests.db-wal
/versions
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
@@ -157,7 +142,6 @@ out
# Nuxt.js build / generate output
.nuxt
dist
dist_optimized
# Gatsby files
.cache/
@@ -304,13 +288,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
/build
.swc
app/(payload)/admin/importMap.js
test/live-preview/app/(payload)/admin/importMap.js
/test/live-preview/app/(payload)/admin/importMap.js
test/admin-root/app/(payload)/admin/importMap.js
/test/admin-root/app/(payload)/admin/importMap.js
test/app/(payload)/admin/importMap.js
/test/app/(payload)/admin/importMap.js
test/pnpm-lock.yaml
test/databaseAdapter.js
.swc

View File

@@ -1 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm run lint-staged --quiet

87
.idea/payload.iml generated
View File

@@ -1,87 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/components" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/dist" />
<excludeFolder url="file://$MODULE_DIR$/.swc" />
<excludeFolder url="file://$MODULE_DIR$/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/examples" />
<excludeFolder url="file://$MODULE_DIR$/media" />
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/fields" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-form-builder/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-sentry/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-stripe/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/dist" />
<excludeFolder url="file://$MODULE_DIR$/templates" />
<excludeFolder url="file://$MODULE_DIR$/test/.swc" />
<excludeFolder url="file://$MODULE_DIR$/versions" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-slate/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-slate/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-nodemailer/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-nodemailer/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-resend/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-resend/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-vue/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-vue/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-form-builder/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-relationship-object-ids/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-relationship-object-ids/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-stripe/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-azure/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-azure/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-gcs/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-gcs/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-s3/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-s3/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-uploadthing/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-uploadthing/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-vercel-blob/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-vercel-blob/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/translations/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/translations/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/dist" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,13 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev Fields" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<arguments value="fields" />
<node-interpreter value="project" />
<envs />
<configuration default="false" name="Run Dev Fields" type="NodeJSConfigurationType" application-parameters="--no-deprecation fields" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
<envs>
<env name="NODE_OPTIONS" value="--no-deprecation" />
</envs>
<method v="2" />
</configuration>
</component>

View File

@@ -1,13 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev _community" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<arguments value="_community" />
<node-interpreter value="project" />
<envs />
<configuration default="false" name="Run Dev _community" type="NodeJSConfigurationType" application-parameters="--no-deprecation _community" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
<envs>
<env name="NODE_OPTIONS" value="--no-deprecation" />
</envs>
<method v="2" />
</configuration>
</component>

View File

@@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev admin" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<arguments value="admin" />
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -1,9 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="true" type="JavaScriptTestRunnerJest">
<node-interpreter value="project" />
<node-options value="--no-deprecation" />
<envs />
<scope-kind value="ALL" />
<method v="2" />
</configuration>
</component>

View File

@@ -1 +1 @@
v22.6.0
v18.19.1

3
.npmrc
View File

@@ -1,3 +1,2 @@
symlink=true
node-linker=isolated
hoist-workspace-packages=false # the default in pnpm v9 is true, but that can break our runtime dependency checks
node-linker=isolated # due to a typescript bug, isolated mode requires @types/express-serve-static-core, terser and monaco-editor to be installed https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189 along with two other changes in the code which I've marked with (tsbugisolatedmode) in the code

2
.nvmrc
View File

@@ -1 +1 @@
v22.6.0
v18.19.1

View File

@@ -10,7 +10,3 @@
**/temp
**/docs/**
tsconfig.json
packages/payload/*.js
packages/payload/*.d.ts
payload-types.ts
tsconfig.tsbuildinfo

9
.swcrc
View File

@@ -7,15 +7,6 @@
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {

View File

@@ -1,2 +0,0 @@
pnpm 9.7.1
nodejs 22.6.0

82
.vscode/launch.json vendored
View File

@@ -10,66 +10,19 @@
"cwd": "${workspaceFolder}"
},
{
"command": "node --no-deprecation test/dev.js _community",
"command": "node --no-deprecation test/dev.js fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Community",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js storage-uploadthing",
"cwd": "${workspaceFolder}",
"name": "Uploadthing",
"request": "launch",
"type": "node-terminal",
"envFile": "${workspaceFolder}/test/storage-uploadthing/.env"
},
{
"command": "node --no-deprecation test/dev.js live-preview",
"command": "pnpm run dev live-preview -- --no-turbo",
"cwd": "${workspaceFolder}",
"name": "Run Dev Live Preview",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/loader/init.js",
"cwd": "${workspaceFolder}",
"name": "Run Loader",
"request": "launch",
"type": "node-terminal",
"env": {
"LOADER_TEST_FILE_PATH": "./dependency-test.js"
// "LOADER_TEST_FILE_PATH": "../fields/config.ts"
}
},
{
"command": "node --no-deprecation test/dev.js admin",
"cwd": "${workspaceFolder}",
"name": "Run Dev Admin",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js auth",
"cwd": "${workspaceFolder}",
"name": "Run Dev Auth",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js fields-relationship",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields-Relationship",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js login-with-username",
"cwd": "${workspaceFolder}",
"name": "Run Dev Login-With-Username",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",
@@ -81,56 +34,49 @@
}
},
{
"command": "node --no-deprecation test/dev.js collections-graphql",
"cwd": "${workspaceFolder}",
"name": "Run Dev GraphQL",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js fields",
"command": "pnpm run dev fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js versions",
"command": "pnpm run dev:postgres versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Postgres",
"request": "launch",
"type": "node-terminal",
"env": {
"PAYLOAD_DATABASE": "postgres"
}
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js versions",
"command": "pnpm run dev versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Versions",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js localization",
"command": "pnpm run dev localization",
"cwd": "${workspaceFolder}",
"name": "Run Dev Localization",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js uploads",
"command": "pnpm run dev uploads",
"cwd": "${workspaceFolder}",
"name": "Run Dev Uploads",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js field-error-states",
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Field Error States",
"name": "Run Dev Fields (Vite)",
"request": "launch",
"type": "node-terminal"
"type": "node-terminal",
"env": {
"NODE_ENV": "production"
}
},
{
"command": "pnpm run test:int live-preview",

16
.vscode/settings.json vendored
View File

@@ -31,15 +31,8 @@
"editor.formatOnSave": true
},
"editor.formatOnSaveMode": "file",
"eslint.rules.customizations": [
// Defaultt all ESLint errors to 'warn' to differentate from TypeScript's 'error' level
{ "rule": "*", "severity": "warn" },
// Silence some warnings that will get auto-fixed
{ "rule": "perfectionist/*", "severity": "off", "fixable": true },
{ "rule": "curly", "severity": "off", "fixable": true },
{ "rule": "object-shorthand", "severity": "off", "fixable": true }
],
// All ESLint rules to 'warn' to differentate from TypeScript's 'error' level
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
"typescript.tsdk": "node_modules/typescript/lib",
// Load .git-blame-ignore-revs file
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
@@ -47,10 +40,5 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"files.insertFinalNewline": true,
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
"jestrunner.debugOptions": {
"runtimeArgs": ["--no-deprecation"]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -63,7 +63,7 @@ Each test directory is split up in this way specifically to reduce friction when
The following command will start Payload with your config: `pnpm dev my-test-dir`. Example: `pnpm dev fields` for the test/`fields` test suite. This command will start up Payload using your config and refresh a test database on every restart. If you're using VS Code, the most common run configs are automatically added to your editor - you should be able to find them in your VS Code launch tab.
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/overview#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/config#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password. These are used in the auto-login.
@@ -120,5 +120,5 @@ This is how you can preview changes you made locally to the docs:
2. Run `yarn install`
3. Duplicate the `.env.example` file and rename it to `.env`
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: *Docs successfully written to /.../website/src/app/docs.json*. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)

View File

@@ -1,7 +1,6 @@
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" /></a>
<br />
<br />
<p align="left">
<a href="https://github.com/payloadcms/payload/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square"></a>
&nbsp;

View File

@@ -1,14 +0,0 @@
import React from 'react'
export const metadata = {
description: 'Generated by Next.js',
title: 'Next.js',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View File

@@ -1,11 +0,0 @@
import configPromise from '@payload-config'
import { getPayloadHMR } from '@payloadcms/next/utilities'
export const Page = async ({ params, searchParams }) => {
const payload = await getPayloadHMR({
config: configPromise,
})
return <div>test ${payload?.config?.collections?.length}</div>
}
export default Page

View File

@@ -1,11 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
import { NotFoundView } from '@payloadcms/next/views/NotFound/index.js'
type Args = {
params: {
@@ -16,10 +12,6 @@ type Args = {
}
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundView({ config, params, searchParams })
export default NotFound

View File

@@ -3,9 +3,7 @@ import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views/Root/index.js'
type Args = {
params: {
@@ -19,7 +17,6 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
export default Page

View File

@@ -1,10 +1,9 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes/index.js'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST } from '@payloadcms/next/routes/index.js'
export const POST = GRAPHQL_POST(config)

View File

@@ -1,8 +0,0 @@
#custom-css {
font-family: monospace;
background-image: url('/placeholder.png');
}
#custom-css::after {
content: 'custom-css';
}

View File

@@ -1,10 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts'
import { importMap } from './admin/importMap.js'
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
import { RootLayout } from '@payloadcms/next/layouts/Root/index.js'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'
@@ -14,10 +10,6 @@ type Args = {
children: React.ReactNode
}
const Layout = ({ children }: Args) => (
<RootLayout config={configPromise} importMap={importMap}>
{children}
</RootLayout>
)
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
export default Layout

View File

@@ -0,0 +1,43 @@
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import React from 'react'
import type { Page as PageType } from '../../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js'
import { Blocks } from '../../_components/Blocks/index.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { Hero } from '../../_components/Hero/index.js'
export const PageClient: React.FC<{
page: PageType
}> = ({ page: initialPage }) => {
const { data } = useLivePreview<PageType>({
depth: 2,
initialData: initialPage,
serverURL: PAYLOAD_SERVER_URL,
})
return (
<React.Fragment>
<Gutter>
<h1 id="page-title">{data.title}</h1>
</Gutter>
<Hero {...data?.hero} />
<Blocks
blocks={[
...(data?.layout ?? []),
{
blockName: 'Relationships',
blockType: 'relationships',
data,
},
]}
disableTopPadding={
!data?.hero || data?.hero?.type === 'none' || data?.hero?.type === 'lowImpact'
}
/>
</React.Fragment>
)
}

View File

@@ -0,0 +1,37 @@
import { notFound } from 'next/navigation.js'
import React from 'react'
import type { Page } from '../../../../test/live-preview/payload-types.js'
import { fetchDoc } from '../../_api/fetchDoc.js'
import { fetchDocs } from '../../_api/fetchDocs.js'
import { PageClient } from './page.client.js'
// eslint-disable-next-line no-restricted-exports
export default async function Page({ params: { slug = 'home' } }) {
let page: Page | null = null
try {
page = await fetchDoc<Page>({
slug,
collection: 'pages',
})
} catch (error) {
console.error(error)
}
if (!page) {
return notFound()
}
return <PageClient page={page} />
}
export async function generateStaticParams() {
try {
const pages = await fetchDocs<Page>('pages')
return pages?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -0,0 +1,68 @@
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import React from 'react'
import type { Post as PostType } from '../../../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
import { Blocks } from '../../../_components/Blocks/index.js'
import { PostHero } from '../../../_heros/PostHero/index.js'
export const PostClient: React.FC<{
post: PostType
}> = ({ post: initialPost }) => {
const { data } = useLivePreview<PostType>({
depth: 2,
initialData: initialPost,
serverURL: PAYLOAD_SERVER_URL,
})
return (
<React.Fragment>
<PostHero post={data} />
<Blocks blocks={data?.layout} />
<Blocks
blocks={[
{
blockName: 'Related Posts',
blockType: 'relatedPosts',
docs: data?.relatedPosts,
introContent: [
{
type: 'h4',
children: [
{
text: 'Related posts',
},
],
},
{
type: 'p',
children: [
{
text: 'The posts displayed here are individually selected for this page. Admins can select any number of related posts to display here and the layout will adjust accordingly. Alternatively, you could swap this out for the "Archive" block to automatically populate posts by category complete with pagination. To manage related posts, ',
},
{
type: 'link',
children: [
{
text: 'navigate to the admin dashboard',
},
],
url: `/admin/collections/posts/${data?.id}`,
},
{
text: '.',
},
],
},
],
relationTo: 'posts',
},
]}
disableTopPadding
/>
</React.Fragment>
)
}

View File

@@ -0,0 +1,36 @@
import { notFound } from 'next/navigation.js'
import React from 'react'
import type { Post } from '../../../../../test/live-preview/payload-types.js'
import { fetchDoc } from '../../../_api/fetchDoc.js'
import { fetchDocs } from '../../../_api/fetchDocs.js'
import { PostClient } from './page.client.js'
export default async function Post({ params: { slug = '' } }) {
let post: Post | null = null
try {
post = await fetchDoc<Post>({
slug,
collection: 'posts',
})
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
if (!post) {
notFound()
}
return <PostClient post={post} />
}
export async function generateStaticParams() {
try {
const posts = await fetchDocs<Post>('posts')
return posts?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -0,0 +1,35 @@
import QueryString from 'qs'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export const fetchDoc = async <T>(args: {
collection: string
depth?: number
id?: string
slug?: string
}): Promise<T> => {
const { id, slug, collection, depth = 2 } = args || {}
const queryString = QueryString.stringify(
{
...(slug ? { 'where[slug][equals]': slug } : {}),
...(depth ? { depth } : {}),
},
{ addQueryPrefix: true },
)
const doc: T = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}${queryString}`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')
return res?.docs?.[0]
})
return doc
}

View File

@@ -0,0 +1,19 @@
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export const fetchDocs = async <T>(collection: string): Promise<T[]> => {
const docs: T[] = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}?depth=0&limit=100`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs')
return res?.docs
})
return docs
}

View File

@@ -0,0 +1,25 @@
import type { Footer } from '../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export async function fetchFooter(): Promise<Footer> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const footer = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/footer`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching footer')
return res
})
return footer
}

View File

@@ -0,0 +1,25 @@
import type { Header } from '../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export async function fetchHeader(): Promise<Header> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const header = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/header`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching header')
return res
})
return header
}

View File

@@ -0,0 +1,46 @@
import React from 'react'
import type { ArchiveBlockProps } from './types.js'
import { CollectionArchive } from '../../_components/CollectionArchive/index.js'
import { Gutter } from '../../_components/Gutter/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
export const ArchiveBlock: React.FC<
ArchiveBlockProps & {
id?: string
}
> = (props) => {
const {
id,
categories,
introContent,
limit,
populateBy,
populatedDocs,
populatedDocsTotal,
relationTo,
selectedDocs,
} = props
return (
<div className={classes.archiveBlock} id={`block-${id}`}>
{introContent && (
<Gutter className={classes.introContent}>
<RichText content={introContent} />
</Gutter>
)}
<CollectionArchive
categories={categories}
limit={limit}
populateBy={populateBy}
populatedDocs={populatedDocs}
populatedDocsTotal={populatedDocsTotal}
relationTo={relationTo}
selectedDocs={selectedDocs}
sort="-publishedDate"
/>
</div>
)
}

View File

@@ -0,0 +1,6 @@
import type { Page } from '../../../../test/live-preview/payload-types.js'
export type ArchiveBlockProps = Extract<
Exclude<Page['layout'], undefined>[0],
{ blockType: 'archive' }
>

View File

@@ -0,0 +1,38 @@
import React from 'react'
import type { Page } from '../../../../test/live-preview/payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { CMSLink } from '../../_components/Link/index.js'
import RichText from '../../_components/RichText/index.js'
import { VerticalPadding } from '../../_components/VerticalPadding/index.js'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'cta' }>
export const CallToActionBlock: React.FC<
Props & {
id?: string
}
> = ({ invertBackground, links, richText }) => {
return (
<Gutter>
<VerticalPadding
className={[classes.callToAction, invertBackground && classes.invert]
.filter(Boolean)
.join(' ')}
>
<div className={classes.wrap}>
<div className={classes.content}>
<RichText className={classes.richText} content={richText} />
</div>
<div className={classes.linkGroup}>
{(links || []).map(({ link }, i) => {
return <CMSLink key={i} {...link} invert={invertBackground} />
})}
</div>
</div>
</VerticalPadding>
</Gutter>
)
}

View File

@@ -0,0 +1,39 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../../test/live-preview/payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { CMSLink } from '../../_components/Link/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'content' }>
export const ContentBlock: React.FC<
Props & {
id?: string
}
> = (props) => {
const { columns } = props
return (
<Gutter className={classes.content}>
<div className={classes.grid}>
{columns && columns.length > 0 ? (
<Fragment>
{columns.map((col, index) => {
const { enableLink, link, richText, size } = col
return (
<div className={[classes.column, classes[`column--${size}`]].join(' ')} key={index}>
<RichText content={richText} />
{enableLink && <CMSLink className={classes.link} {...link} />}
</div>
)
})}
</Fragment>
) : null}
</div>
</Gutter>
)
}

View File

@@ -0,0 +1,42 @@
import type { StaticImageData } from 'next/image.js'
import React from 'react'
import type { Page } from '../../../../test/live-preview/payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { Media } from '../../_components/Media/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'mediaBlock' }> & {
id?: string
staticImage?: StaticImageData
}
export const MediaBlock: React.FC<Props> = (props) => {
const { media, position = 'default', staticImage } = props
let caption
if (media && typeof media === 'object') caption = media.caption
return (
<div className={classes.mediaBlock}>
{position === 'fullscreen' && (
<div className={classes.fullscreen}>
<Media resource={media} src={staticImage} />
</div>
)}
{position === 'default' && (
<Gutter>
<Media resource={media} src={staticImage} />
</Gutter>
)}
{caption && (
<Gutter className={classes.caption}>
<RichText content={caption} />
</Gutter>
)}
</div>
)
}

View File

@@ -0,0 +1,52 @@
import React from 'react'
import type { Post } from '../../../../test/live-preview/payload-types.js'
import { Card } from '../../_components/Card/index.js'
import { Gutter } from '../../_components/Gutter/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
export type RelatedPostsProps = {
blockName: string
blockType: 'relatedPosts'
docs?: (Post | string)[] | null
introContent?: any
relationTo: 'posts'
}
export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
const { docs, introContent, relationTo } = props
return (
<div className={classes.relatedPosts}>
{introContent && (
<Gutter className={classes.introContent}>
<RichText content={introContent} />
</Gutter>
)}
<Gutter>
<div className={classes.grid}>
{docs?.map((doc, index) => {
if (typeof doc === 'string') return null
return (
<div
className={[
classes.column,
docs.length === 2 && classes['cols-half'],
docs.length >= 3 && classes['cols-thirds'],
]
.filter(Boolean)
.join(' ')}
key={index}
>
<Card doc={doc} relationTo={relationTo} showCategories />
</div>
)
})}
</div>
</Gutter>
</div>
)
}

View File

@@ -0,0 +1,196 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../../test/live-preview/payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
export type RelationshipsBlockProps = {
blockName: string
blockType: 'relationships'
data: Page
}
export const RelationshipsBlock: React.FC<RelationshipsBlockProps> = (props) => {
const { data } = props
return (
<div className={classes.relationshipsBlock}>
<Gutter>
<p>
This block is for testing purposes only. It renders every possible type of relationship.
</p>
<p>
<b>Rich Text Slate:</b>
</p>
{data?.richTextSlate && <RichText content={data.richTextSlate} renderUploadFilenameOnly />}
<p>
<b>Rich Text Lexical:</b>
</p>
{data?.richTextLexical && (
<RichText content={data.richTextLexical} renderUploadFilenameOnly serializer="lexical" />
)}
<p>
<b>Upload:</b>
</p>
{data?.relationshipAsUpload ? (
<div>
{typeof data?.relationshipAsUpload === 'string'
? data?.relationshipAsUpload
: data?.relationshipAsUpload.filename}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has One:</b>
</p>
{data?.relationshipMonoHasOne ? (
<div>
{typeof data?.relationshipMonoHasOne === 'string'
? data?.relationshipMonoHasOne
: data?.relationshipMonoHasOne.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has Many:</b>
</p>
{data?.relationshipMonoHasMany ? (
<Fragment>
{data?.relationshipMonoHasMany.length
? data?.relationshipMonoHasMany?.map((item, index) =>
item ? (
<div key={index}>{typeof item === 'string' ? item : item.title}</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has One:</b>
</p>
{data?.relationshipPolyHasOne ? (
<div>
{typeof data?.relationshipPolyHasOne.value === 'string'
? data?.relationshipPolyHasOne.value
: data?.relationshipPolyHasOne.value.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has Many:</b>
</p>
{data?.relationshipPolyHasMany ? (
<Fragment>
{data?.relationshipPolyHasMany.length
? data?.relationshipPolyHasMany?.map((item, index) =>
item.value ? (
<div key={index}>
{typeof item.value === 'string' ? item.value : item.value.title}
</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
<p>
<b>Array of Relationships:</b>
</p>
{data?.arrayOfRelationships?.map((item, index) => (
<div className={classes.array} key={index}>
<p>
<b>Rich Text:</b>
</p>
{item?.richTextInArray && <RichText content={item.richTextInArray} />}
<p>
<b>Upload:</b>
</p>
{item?.uploadInArray ? (
<div>
{typeof item?.uploadInArray === 'string'
? item?.uploadInArray
: item?.uploadInArray.filename}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has One:</b>
</p>
{item?.relationshipInArrayMonoHasOne ? (
<div>
{typeof item?.relationshipInArrayMonoHasOne === 'string'
? item?.relationshipInArrayMonoHasOne
: item?.relationshipInArrayMonoHasOne.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has Many:</b>
</p>
{item?.relationshipInArrayMonoHasMany ? (
<Fragment>
{item?.relationshipInArrayMonoHasMany.length
? item?.relationshipInArrayMonoHasMany?.map((rel, relIndex) =>
rel ? (
<div key={relIndex}>{typeof rel === 'string' ? rel : rel.title}</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has One:</b>
</p>
{item?.relationshipInArrayPolyHasOne ? (
<div>
{typeof item?.relationshipInArrayPolyHasOne.value === 'string'
? item?.relationshipInArrayPolyHasOne.value
: item?.relationshipInArrayPolyHasOne.value.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has Many:</b>
</p>
{item?.relationshipInArrayPolyHasMany ? (
<Fragment>
{item?.relationshipInArrayPolyHasMany.length
? item?.relationshipInArrayPolyHasMany?.map((rel, relIndex) =>
rel.value ? (
<div key={relIndex}>
{typeof rel.value === 'string' ? rel.value : rel.value.title}
</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
</div>
))}
</Gutter>
</div>
)
}

View File

@@ -0,0 +1,88 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../../test/live-preview/payload-types.js'
import type { RelationshipsBlockProps } from '../../_blocks/Relationships/index.js'
import type { VerticalPaddingOptions } from '../VerticalPadding/index.js'
import { ArchiveBlock } from '../../_blocks/ArchiveBlock/index.js'
import { CallToActionBlock } from '../../_blocks/CallToAction/index.js'
import { ContentBlock } from '../../_blocks/Content/index.js'
import { MediaBlock } from '../../_blocks/MediaBlock/index.js'
import { RelatedPosts, type RelatedPostsProps } from '../../_blocks/RelatedPosts/index.js'
import { RelationshipsBlock } from '../../_blocks/Relationships/index.js'
import { toKebabCase } from '../../_utilities/toKebabCase.js'
import { BackgroundColor } from '../BackgroundColor/index.js'
import { VerticalPadding } from '../VerticalPadding/index.js'
const blockComponents = {
archive: ArchiveBlock,
content: ContentBlock,
cta: CallToActionBlock,
mediaBlock: MediaBlock,
relatedPosts: RelatedPosts,
relationships: RelationshipsBlock,
}
type Block = NonNullable<Page['layout']>[number]
export const Blocks: React.FC<{
blocks?: (Block | RelatedPostsProps | RelationshipsBlockProps)[] | null
disableTopPadding?: boolean
}> = (props) => {
const { blocks, disableTopPadding } = props
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
if (hasBlocks) {
return (
<Fragment>
{blocks.map((block, index) => {
const { blockName, blockType } = block
if (blockType && blockType in blockComponents) {
const Block = blockComponents[blockType]
// the cta block is containerized, so we don't consider it to be inverted at the block-level
const blockIsInverted =
'invertBackground' in block && blockType !== 'cta' ? block.invertBackground : false
const prevBlock = blocks[index - 1]
const prevBlockInverted =
prevBlock && 'invertBackground' in prevBlock && prevBlock?.invertBackground
const isPrevSame = Boolean(blockIsInverted) === Boolean(prevBlockInverted)
let paddingTop: VerticalPaddingOptions = 'large'
let paddingBottom: VerticalPaddingOptions = 'large'
if (prevBlock && isPrevSame) {
paddingTop = 'none'
}
if (index === blocks.length - 1) {
paddingBottom = 'large'
}
if (disableTopPadding && index === 0) {
paddingTop = 'none'
}
if (Block) {
return (
<BackgroundColor invert={blockIsInverted} key={index}>
<VerticalPadding bottom={paddingBottom} top={paddingTop}>
{/* @ts-expect-error */}
<Block id={toKebabCase(blockName)} {...block} />
</VerticalPadding>
</BackgroundColor>
)
}
}
return null
})}
</Fragment>
)
}
return null
}

View File

@@ -0,0 +1,88 @@
import LinkWithDefault from 'next/link.js'
import React, { Fragment } from 'react'
import type { Post } from '../../../../test/live-preview/payload-types.js'
import { Media } from '../Media/index.js'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
export const Card: React.FC<{
alignItems?: 'center'
className?: string
doc?: Post
hideImagesOnMobile?: boolean
orientation?: 'horizontal' | 'vertical'
relationTo?: 'posts'
showCategories?: boolean
title?: string
}> = (props) => {
const {
className,
doc,
orientation = 'vertical',
relationTo,
showCategories,
title: titleFromProps,
} = props
const { slug, categories, meta, title } = doc || {}
const { description, image: metaImage } = meta || {}
const hasCategories = categories && Array.isArray(categories) && categories.length > 0
const titleToUse = titleFromProps || title
const sanitizedDescription = description?.replace(/\s/g, ' ') // replace non-breaking space with white space
const href = `/live-preview/${relationTo}/${slug}`
return (
<div
className={[classes.card, className, orientation && classes[orientation]]
.filter(Boolean)
.join(' ')}
>
<Link className={classes.mediaWrapper} href={href}>
{!metaImage && <div className={classes.placeholder}>No image</div>}
{metaImage && typeof metaImage !== 'string' && (
<Media fill imgClassName={classes.image} resource={metaImage} />
)}
</Link>
<div className={classes.content}>
{showCategories && hasCategories && (
<div className={classes.leader}>
{showCategories && hasCategories && (
<div>
{categories?.map((category, index) => {
const titleFromCategory = typeof category === 'string' ? category : category.title
const categoryTitle = titleFromCategory || 'Untitled category'
const isLast = index === categories.length - 1
return (
<Fragment key={index}>
{categoryTitle}
{!isLast && <Fragment>, &nbsp;</Fragment>}
</Fragment>
)
})}
</div>
)}
</div>
)}
{titleToUse && (
<h4 className={classes.title}>
<Link className={classes.titleLink} href={href}>
{titleToUse}
</Link>
</h4>
)}
{description && (
<div className={classes.body}>
{description && <p className={classes.description}>{sanitizedDescription}</p>}
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,186 @@
'use client'
import qs from 'qs'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import type { Post } from '../../../../../test/live-preview/payload-types.js'
import type { ArchiveBlockProps } from '../../../_blocks/ArchiveBlock/types.js'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
import { Card } from '../../Card/index.js'
import { Gutter } from '../../Gutter/index.js'
import { PageRange } from '../../PageRange/index.js'
import { Pagination } from '../../Pagination/index.js'
import classes from './index.module.scss'
type Result = {
docs: (Post | string)[]
hasNextPage: boolean
hasPrevPage: boolean
nextPage: number
page: number
prevPage: number
totalDocs: number
totalPages: number
}
export type Props = Omit<ArchiveBlockProps, 'blockType'> & {
className?: string
onResultChange?: (result: Result) => void // eslint-disable-line no-unused-vars
showPageRange?: boolean
sort?: string
}
export const CollectionArchiveByCollection: React.FC<Props> = (props) => {
const {
categories: catsFromProps,
className,
limit = 10,
onResultChange,
populatedDocs,
populatedDocsTotal,
relationTo,
showPageRange,
sort = '-createdAt',
} = props
const [results, setResults] = useState<Result>({
docs: populatedDocs?.map((doc) => doc.value) || [],
hasNextPage: false,
hasPrevPage: false,
nextPage: 1,
page: 1,
prevPage: 1,
totalDocs: typeof populatedDocsTotal === 'number' ? populatedDocsTotal : 0,
totalPages: 1,
})
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | undefined>(undefined)
const scrollRef = useRef<HTMLDivElement>(null)
const hasHydrated = useRef(false)
const [page, setPage] = useState(1)
const scrollToRef = useCallback(() => {
const { current } = scrollRef
if (current) {
// current.scrollIntoView({
// behavior: 'smooth',
// })
}
}, [])
useEffect(() => {
if (!isLoading && typeof results.page !== 'undefined') {
// scrollToRef()
}
}, [isLoading, scrollToRef, results])
useEffect(() => {
// hydrate the block with fresh content after first render
// don't show loader unless the request takes longer than x ms
// and don't show it during initial hydration
const timer = setTimeout(() => {
if (hasHydrated) {
setIsLoading(true)
}
}, 500)
const searchQuery = qs.stringify(
{
depth: 1,
limit,
page,
sort,
where: {
...(catsFromProps && catsFromProps?.length > 0
? {
categories: {
in:
typeof catsFromProps === 'string'
? [catsFromProps]
: catsFromProps
.map((cat) => (typeof cat === 'object' && cat !== null ? cat.id : cat))
.join(','),
},
}
: {}),
},
},
{ encode: false },
)
const makeRequest = async () => {
try {
const req = await fetch(`${PAYLOAD_SERVER_URL}/api/${relationTo}?${searchQuery}`)
const json = await req.json()
clearTimeout(timer)
hasHydrated.current = true
const { docs } = json as { docs: Post[] }
if (docs && Array.isArray(docs)) {
setResults(json)
setIsLoading(false)
if (typeof onResultChange === 'function') {
onResultChange(json)
}
}
} catch (err) {
console.warn(err) // eslint-disable-line no-console
setIsLoading(false)
setError(`Unable to load "${relationTo} archive" data at this time.`)
}
}
void makeRequest()
return () => {
if (timer) clearTimeout(timer)
}
}, [page, catsFromProps, relationTo, onResultChange, sort, limit])
return (
<div className={[classes.collectionArchive, className].filter(Boolean).join(' ')}>
<div className={classes.scrollRef} ref={scrollRef} />
{!isLoading && error && <Gutter>{error}</Gutter>}
<Fragment>
{showPageRange !== false && (
<Gutter>
<div className={classes.pageRange}>
<PageRange
collection={relationTo}
currentPage={results.page}
limit={limit}
totalDocs={results.totalDocs}
/>
</div>
</Gutter>
)}
<Gutter>
<div className={classes.grid}>
{results.docs?.map((result, index) => {
if (typeof result === 'string') {
return null
}
return (
<div className={classes.column} key={index}>
<Card doc={result} relationTo="posts" showCategories />
</div>
)
})}
</div>
{results.totalPages > 1 && (
<Pagination
className={classes.pagination}
onClick={setPage}
page={results.page}
totalPages={results.totalPages}
/>
)}
</Gutter>
</Fragment>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import React from 'react'
import type { ArchiveBlockProps } from '../../_blocks/ArchiveBlock/types.js'
import { CollectionArchiveByCollection } from './PopulateByCollection/index.js'
import { CollectionArchiveBySelection } from './PopulateBySelection/index.js'
export type Props = Omit<ArchiveBlockProps, 'blockType'> & {
className?: string
sort?: string
}
export const CollectionArchive: React.FC<Props> = (props) => {
const { className, populateBy, selectedDocs } = props
if (populateBy === 'selection') {
return <CollectionArchiveBySelection className={className} selectedDocs={selectedDocs} />
}
if (populateBy === 'collection') {
return <CollectionArchiveByCollection {...props} className={className} />
}
return null
}

View File

@@ -0,0 +1,41 @@
import LinkWithDefault from 'next/link.js'
import React from 'react'
import { fetchFooter } from '../../_api/fetchFooter.js'
import { Gutter } from '../Gutter/index.js'
import { CMSLink } from '../Link/index.js'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
export async function Footer() {
const footer = await fetchFooter()
const navItems = footer?.navItems || []
return (
<footer className={classes.footer}>
<Gutter className={classes.wrap}>
<Link href="/">
<picture>
<img
alt="Payload Logo"
className={classes.logo}
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
/>
</picture>
</Link>
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
<Link href="/admin">Admin</Link>
<Link href="https://github.com/payloadcms/payload/tree/main/templates/ecommerce">
Source Code
</Link>
<Link href="https://github.com/payloadcms/payload">Payload</Link>
</nav>
</Gutter>
</footer>
)
}

View File

@@ -0,0 +1,28 @@
import LinkWithDefault from 'next/link.js'
import React from 'react'
import { fetchHeader } from '../../_api/fetchHeader.js'
import { Gutter } from '../Gutter/index.js'
import { HeaderNav } from './Nav/index.js'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
export async function Header() {
const header = await fetchHeader()
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link href="/live-preview">
<img
alt="Payload Logo"
className={classes.logo}
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-dark.svg"
/>
</Link>
<HeaderNav header={header} />
</Gutter>
</header>
)
}

View File

@@ -0,0 +1,67 @@
import LinkWithDefault from 'next/link.js'
import React from 'react'
import type { Page, Post } from '../../../../test/live-preview/payload-types.js'
import type { Props as ButtonProps } from '../Button/index.js'
import { Button } from '../Button/index.js'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
type CMSLinkType = {
appearance?: ButtonProps['appearance']
children?: React.ReactNode
className?: string
invert?: ButtonProps['invert']
label?: string
newTab?: boolean
reference?: {
relationTo: 'pages' | 'posts'
value: Page | Post | string
}
type?: 'custom' | 'reference'
url?: string
}
export const CMSLink: React.FC<CMSLinkType> = ({
type,
appearance,
children,
className,
invert,
label,
newTab,
reference,
url,
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `/${reference.value.slug}`
: url
if (!href) return null
if (!appearance) {
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (href || url) {
return (
<Link {...newTabProps} className={className} href={href || url || ''}>
{label && label}
{children && children}
</Link>
)
}
}
return (
<Button
appearance={appearance}
className={className}
href={href}
invert={invert}
label={label}
newTab={newTab}
/>
)
}

View File

@@ -0,0 +1,31 @@
import type { ElementType } from 'react'
import React, { Fragment } from 'react'
import type { Props } from './types.js'
import { Image } from './Image/index.js'
import { Video } from './Video/index.js'
export const Media: React.FC<Props> = (props) => {
const { className, htmlElement = 'div', resource } = props
const isVideo = typeof resource !== 'string' && resource?.mimeType?.includes('video')
const Tag = (htmlElement as ElementType) || Fragment
return (
<Tag
{...(htmlElement !== null
? {
className,
}
: {})}
>
{isVideo ? (
<Video {...props} />
) : (
<Image {...props} /> // eslint-disable-line
)}
</Tag>
)
}

View File

@@ -0,0 +1,20 @@
import type { StaticImageData } from 'next/image'
import type { ElementType, Ref } from 'react'
import type { Media as MediaType } from '../../../payload-types'
export interface Props {
alt?: string
className?: string
fill?: boolean // for NextImage only
htmlElement?: ElementType | null
imgClassName?: string
onClick?: () => void
onLoad?: () => void
priority?: boolean // for NextImage only
ref?: Ref<HTMLImageElement | HTMLVideoElement | null>
resource?: MediaType | string // for Payload media
size?: string // for NextImage only
src?: StaticImageData // for static media
videoClassName?: string
}

Some files were not shown because too many files have changed in this diff Show More