Compare commits

..

74 Commits

Author SHA1 Message Date
Elliot DeNolf
4e0dfd410d chore(release): v3.0.0-beta.37 [skip ci] 2024-05-29 10:54:45 -04:00
Elliot DeNolf
8506385ef9 fix(cpa): improve package manager detection (#6546)
Improves package manager detection.

Closes #6231
2024-05-29 09:30:15 -04:00
Jarrod Flesch
e74952902e fix: multi value draggable/sortable pills (#6500) 2024-05-29 08:22:37 -04:00
Alessio Gravili
4a51f4d2c1 fix(richtext-lexical): various html converter fixes (#6544) 2024-05-29 00:29:25 -04:00
Alessio Gravili
2c283bcc08 fix(richtext-lexical): user-defined html converters not taking precedence, and shared default html converters doubling in size after every field initialization 2024-05-29 00:14:58 -04:00
Alessio Gravili
a2e9bcd333 fix(richtext-lexical): list converters and nodes being added duplicatively 2024-05-28 23:53:35 -04:00
Alessio Gravili
33d53121a2 feat(richtext-lexical): link markdown transformers (#6543)
Closes https://github.com/payloadcms/payload/issues/6507

---------

Co-authored-by: ShawnVogt <41651465+shawnvogt@users.noreply.github.com>
2024-05-29 03:28:26 +00:00
Leo Hilsheimer
e0b201c810 fix(richtext-lexical): link html converter: serialize newTab to target="_blank" (#6350)
Co-authored-by: Leo <leo.hilsheimer@gmail.com>
2024-05-28 23:20:44 -04:00
Alessio Gravili
a8000f644f feat(richtext-lexical): i18n (#6542)
Continuation of https://github.com/payloadcms/payload/pull/6524
2024-05-29 02:40:48 +00:00
Paul
7d0e909a30 feat(plugin-form-builder)!: update form builder plugin field overrides to use a function instead (#6497)
## Description

Changes the `fields` override for form builder plugin to use a function
instead so that we can actually override existing fields which currently
will not work.

```ts
//before
fields: [
  {
    name: 'custom',
    type: 'text',
  }
]

// current
fields: ({ defaultFields }) => {
  return [
    ...defaultFields,
    {
      name: 'custom',
      type: 'text',
    },
  ]
}
```

## Type of change

- [x] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
2024-05-28 17:45:51 -03:00
Elliot DeNolf
b2662eeb1f ci: update app-build-with-packed job (#6541)
Add `--ignore-workspace` and `--no-frozen-lockfile` where necessary
2024-05-28 14:35:18 -04:00
Elliot DeNolf
0b274dd67e chore: adjust email-nodemailer workspace dep pattern (#6539)
Adjust dep pattern for email-nodemailer reference from plugin-cloud
2024-05-28 14:19:21 -04:00
Elliot DeNolf
2ddd50edc4 fix(deps): proper location for scheduler peer dep (#6537)
Properly put `scheduler` dep under `ui` instead of `payload`.
2024-05-28 14:15:56 -04:00
Elliot DeNolf
0287acb8f0 chore(templates): update dockerfile and docker-compose for blank template (#6536)
Update Dockerfile and docker-compose.yml for blank template.
2024-05-28 12:35:05 -04:00
Elliot DeNolf
10c94b3665 feat(cpa): update existing payload installation (#6193)
Updates create-payload-app to update an existing payload installation

- Detects existing Payload installation. Fixes #6517 
- If not latest, will install latest and grab the `(payload)` directory
structure (ripped from `templates/blank-3.0`
2024-05-28 11:38:33 -04:00
Alessio Gravili
ea48ca377e chore: move lexical package from workspace-root to test package (#6533) 2024-05-28 15:01:30 +00:00
zvizvi
6f5d86ed84 fix: Add missing He lang export in payload/i18n (#6484)
## Description
Fixed missing Hebrew language export in payload/i18n module.
The import statement import { he } from 'payload/i18n/he' was not
functioning due to he not being exported correctly.


<!-- Please include a summary of the pull request and any related issues
it fixes. Please also include relevant motivation and context. -->

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] Chore (non-breaking change which does not add functionality)
2024-05-28 10:52:20 -03:00
Alessio Gravili
c383f391e3 feat(richtext-lexical): i18n support (#6524) 2024-05-28 09:10:39 -04:00
Paul
8a91a7adbb feat(richtext-lexical): update validation of custom URLs to include relative and anchor links (#6525)
Updates the regex to allow relative and anchor links as well. Manually
tested all common variations of absolute, relative and anchor links with
a combination

## Type of change

- [x] New feature (non-breaking change which adds functionality)
2024-05-28 00:55:00 -03:00
Alessio Gravili
96181d91a6 chore(ui): add ability to compile using react compiler (#6483)
This does not enable the react compiler by default
2024-05-27 22:54:36 -04:00
Alessio Gravili
eff5129a5f fix(next): unable to pass custom view client components (#6513) 2024-05-27 22:48:41 -04:00
Dan Ribbens
38e5adc462 chore: fix seed file path windows (#6512) 2024-05-26 15:39:16 +00:00
Dan Ribbens
ff4ea1eecc chore: getPayloadHMR conditionally call db.connect (#6510) 2024-05-25 22:17:27 +00:00
Dan Ribbens
dbfd1beed5 chore: gitignore static files uploads tests (#6509) 2024-05-25 22:06:43 +00:00
Dan Ribbens
4b6774463e chore: loader tests error on windows (#6508) 2024-05-25 21:57:32 +00:00
Dan Ribbens
cb14b97a6e chore: swcrc syntax fix (#6505) 2024-05-25 15:45:05 +00:00
Jarrod Flesch
18bc4b708c fix: separate collection docs with same ids were excluded in selectable (#6499) 2024-05-24 15:20:07 -04:00
Paul
6d951e6987 chore: add lexical as a direct dependency to the website template (#6496) 2024-05-24 16:33:36 +00:00
Elliot DeNolf
365660764d chore(templates): enable next lint on blank (#6494)
Enables next linting on blank template

Closes #6481
2024-05-24 11:36:50 -04:00
Elliot DeNolf
8b91af8a5b chore(cpa): adjust unit test template (#6490)
Adjust template used in unit tests.
2024-05-24 10:12:19 -04:00
Paul
b4092f59ae chore: fix seed data validation in website template (#6491)
Fixes an issue with data validation in lexical for the seed script
2024-05-24 14:10:29 +00:00
Alessio Gravili
7a768144ea fix(richtext-lexical): localized sub-fields were omitted from the API output (#6489)
Closes #6455. Proper localization support will be worked on later, this
just resolves the issue where having it enabled not only doesn't
localize those fields, it also omits them from the API response. Now,
they are not omitted, and localization is simply skipped.
2024-05-24 10:01:04 -04:00
Elliot DeNolf
3839eb5ab0 chore(templates): remove blank v2 template (#6488)
New v3 is `blank-3.0`. Will rename that one in future PR.
2024-05-24 09:36:57 -04:00
Paul
fd02bee0fe chore: website template updates (#6480)
Just style updates
2024-05-23 20:38:25 +00:00
Patrik
42222cd2f6 fix(ui): where builder issues (#6478)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-05-23 16:01:13 -04:00
Elliot DeNolf
e3222f2ac3 chore(release): v3.0.0-beta.36 [skip ci] 2024-05-23 13:35:19 -04:00
Alessio Gravili
35f961fecb feat!: next.js 15, react 19, react compiler support (#6429)
**BREAKING:**
- bumps minimum required next.js version from `14.3.0-canary.68` to
`15.0.0-rc.0`
- bumps minimum required react and react-dom versions to `19.0.0
`(`19.0.0-rc-f994737d14-20240522` should be used)
- `@types/react` and `@types/react-dom` have to be bumped to
`npm:types-react@19.0.0-beta.2` using overrides and pnpm overrides, if
you want correct types. You can find an example of this here:
https://github.com/payloadcms/payload/pull/6429/files#diff-10cb9e57a77733f174ee2888587281e94c31f79e434aa3f932a8ec72fa7a5121L32

## Issues

- Bunch of todos for our react-select package which is having type
issues. Works fine, just type issues. Their type defs are importing JSX
in a weird way, we likely just have to wait until they fix them in a
future update.
2024-05-23 13:30:12 -04:00
Paul
85bfca79ef feat: add website template (#6470)
Adds the new website template for 3.0
2024-05-23 16:48:41 +00:00
Alessio Gravili
661a4a099d feat(ui): split up Select component into Select and SelectInput (#6471) 2024-05-23 11:36:57 -04:00
Jarrod Flesch
72c0534008 fix: adds host to initPage req creation (#6476) 2024-05-23 11:04:35 -04:00
Alessio Gravili
78579ed2bd feat(richtext-lexical): various UX and performance improvements (#6467) 2024-05-22 14:42:17 -04:00
Alessio Gravili
7bcb4ba1cc chore(email-*): remove excess backtick in readme install commands 2024-05-22 13:59:33 -04:00
Alessio Gravili
6b45cf3197 feat(richtext-lexical): improve block dragging UX 2024-05-22 13:55:44 -04:00
Elliot DeNolf
73d0b209d7 fix: isHotkey webpack error (#6466)
Fixes webpack issue with isHotkey: `TypeError:
is_hotkey__WEBPACK_IMPORTED_MODULE_9__ is not a function`

Changing this from a default import to a named export, and it appears to
resolve the issue.

Fixes #6421
2024-05-22 17:41:15 +00:00
Alessio Gravili
c93752bdbb fix(richtext-lexical): order of add/drag handles was inconsistent between gutter and no-gutter mode 2024-05-22 10:49:11 -04:00
Alessio Gravili
7a4dd5890e fix(ui): field errors aren't red in light mode 2024-05-22 10:29:41 -04:00
Alessio Gravili
60ee55fcaa chore(richtext-lexical): do not show red border & background for erroring field without gutter 2024-05-22 10:22:06 -04:00
Jacob Fletcher
1fe9790d0d feat(next): server-side theme detection (#6452) 2024-05-22 10:19:38 -04:00
zvizvi
3c0853a675 feat(translations): add Hebrew translation (#6428)
Hebrew translation added.
2024-05-22 14:15:10 +00:00
Jacob Fletcher
2b941b7e2c fix(next,ui): fixes global doc permissions and optimizes publish access data loading (#6451) 2024-05-22 10:03:12 -04:00
Elliot DeNolf
db772a058c chore: add label-author.yml 2024-05-22 09:09:52 -04:00
Alessio Gravili
0bfbf9c750 fix(richtext-lexical): link drawer sending too many form state requests for actions unrelated to links 2024-05-21 22:34:41 -04:00
Alessio Gravili
5c7647f45b ci: split up test suites (#6415) 2024-05-21 17:11:55 -04:00
Alessio Gravili
6c952875e8 feat(richtext-lexical): various gutter, error states & add/drag handle improvements (#6448)
## Gutter

Adds gutter by default:

![CleanShot 2024-05-21 at 16 24
13](https://github.com/payloadcms/payload/assets/70709113/09c59b6f-bd4a-4e81-bfdd-731d1cbbe075)


![CleanShot 2024-05-21 at 16 20
23](https://github.com/payloadcms/payload/assets/70709113/94df3e8c-663e-4b08-90cb-a24b2a788ff6)

can be disabled with admin.hideGutter

## Error states
![CleanShot 2024-05-21 at 16 21
18](https://github.com/payloadcms/payload/assets/70709113/06754d8f-c674-4aaa-a4e5-47e284970776)

Finally, proper error states display. Cleaner, and previously fields
were shown as erroring even though they weren't. No more!

## Drag & Block handles
Improved performance, and cleaned up code. Drag handle positions are now
only calculated for one editor rather than all editors on the page. Add
block handle calculation now uses a better algorithm to minimize the
amount of nodes which are iterated.

Additionally, have you noticed how sometimes the add button jumps to the
next node while the drag button is still at the previous node?


https://github.com/payloadcms/payload/assets/70709113/8dff3081-1de0-4902-8229-62f178f23549

No more! Now they behave the same. Feels a lot cleaner now.
2024-05-21 20:55:06 +00:00
Jacob Fletcher
af7e12aa2f chore(ui)!: uses consistent button naming conventions (#6444)
## Description

Renames the `Save` to `SaveButton`, etc. to match the already
established convention of the `PreviewButton`, etc. This matches the
imports with their respective component and type names, and also gives
these components more context to the developer whenever they're
rendered, i.e. its clearly just a button and not an entire block or
complex component.

**BREAKING**:

Import paths for these components have changed, if you were previously
importing these components into your own projects to customize, change
the import paths accordingly:

Old:
```ts
import { PublishButton } from '@payloadcms/ui/elements/Publish'
import { SaveButton } from '@payloadcms/ui/elements/Save'
import { SaveDraftButton } from '@payloadcms/ui/elements/SaveDraft'
```

New:
```ts
import { PublishButton } from '@payloadcms/ui/elements/PublishButton'
import { SaveButton } from '@payloadcms/ui/elements/SaveButton'
import { SaveDraftButton } from '@payloadcms/ui/elements/SaveDraftButton'
```

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.
2024-05-21 14:52:53 -04:00
Patrik
bcc506b423 fix(ui): disableListColumn fields not hidden in table columns (#6445)
## Description

Setting `disableListColumn` to `true` on a field would hide the field
from the column selector but not from the table columns.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-05-21 13:33:28 -04:00
Elliot DeNolf
3d7c8277d7 chore(release): v3.0.0-beta.35 [skip ci] 2024-05-21 10:51:19 -04:00
Paul
a8a2dc2347 chore!: export DefaultListView as ListView (#6432)
Change the exports of DefaultListView and DefaultEditView to be renamed
without "Default" as ListView

```ts
// before
import { DefaultEditView } from '@payloadcms/next/views'
import { DefaultListView } from '@payloadcms/next/views'

// after 
import { EditView } from '@payloadcms/next/views'
import { ListView } from '@payloadcms/next/views'
```
2024-05-21 10:22:44 -04:00
Alessio Gravili
6c74b0326b chore(richtext-lexical): improve node validation messages (#6443) 2024-05-21 10:19:24 -04:00
Paul
f51af92491 chore(translations): ai translation script should use formal language (#6433)
Added additional prompt to make sure the translation we receive is using
formal language where it makes sense.

In the context of latin languages for example:
- Spanish: "tu" should be using "vos"
- French: "tu" should be using "votre"

These differences can affect verb conjugations and in these languages it
comes across as less professional if informal language is used.
2024-05-21 10:15:01 -04:00
Alessio Gravili
77528a1e7d chore(richtext-slate): fix richtext container elements direction 2024-05-21 09:40:17 -04:00
Alessio Gravili
ba8b8e8330 chore(richtext-lexical): improve node validation messages 2024-05-21 09:36:58 -04:00
Jessica Chowdhury
23f9a32a99 fix: user verification email broken (#6442)
## Description

Closes
[#225](https://github.com/payloadcms/payload-3.0-demo/issues/225).

The user verification emails are not being sent and this error is shown:
```ts
APIError: Error sending email: 422 validation_error - Invalid `from` field. The email address needs to follow the `email@example.com` or `Name <email@example.com>` format.
```

The issue is resolved by updating the `from` property on the outgoing
verification email:
```ts
from: `"${email.defaultFromName}" <${email.defaultFromName}>`,
// to
from: `"${email.defaultFromName}" <${email. defaultFromAddress}>`,
```

**NOTE:** This was not broken in 2.0, see correct outgoing email
[here](https://github.com/payloadcms/payload/blob/main/packages/payload/src/auth/sendVerificationEmail.ts#L69).

- [X] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [X] Existing test suite passes locally with my changes
2024-05-21 13:25:59 +00:00
Jessica Chowdhury
0190eb8b28 fix(ui): blocks browser save dialog from opening when hotkey used with no changes (#6366) 2024-05-21 09:00:34 -04:00
Anders Semb Hermansen
f482fdcfd5 fix: separate sort and search fields when looking up relationship. (#6440)
## Description

Default sort is used as searching field which is causing unexpected
behaviour described in https://github.com/payloadcms/payload/issues/4815
and https://github.com/payloadcms/payload/issues/5222 This bugfix
separates which field is used for sorting and which is used for
searching.

Fixes: https://github.com/payloadcms/payload/issues/4815
https://github.com/payloadcms/payload/issues/5222

@denolfe This fix is a port of the fix in
[#5964](https://github.com/payloadcms/payload/pull/5964) to beta branch.

- [X] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-05-20 16:57:49 -04:00
Alessio Gravili
ed4766188d fix(ui): tooltip positioning issues (#6439) 2024-05-20 20:37:53 +00:00
Ritsu
e682cb1b04 fix(ui): update relationship cell formatted value when when search changes (#6208)
## Description

Fixes https://github.com/payloadcms/payload-3.0-demo/issues/181
Although issue is about page changing, it happens as well when you
change sort / limit / where filter (and probably locale)
<!-- Please include a summary of the pull request and any related issues
it fixes. Please also include relevant motivation and context. -->

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->


- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes

---------

Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
2024-05-20 16:03:04 -04:00
Elliot DeNolf
36fda30c61 feat: store focal point on uploads (#6436)
Store focal point data on uploads as `focalX` and `focalY`

Addresses https://github.com/payloadcms/payload/discussions/4082

Mirrors #6364 for beta branch.
2024-05-20 15:57:52 -04:00
Alessio Gravili
fa7cc376d1 fix(richtext-lexical): field required validation not working if content was removed manually (#6435) 2024-05-20 17:17:54 +00:00
Paul
3fc2ff1ef9 chore: export DefaultListView for reuse (#6422)
Exports `DefaultListView` so other plugins or custom implementations can
re-use it
2024-05-20 11:53:36 -03:00
Jarrod Flesch
1d81eef805 fix: attributes graphql packages, adds esm import path (#6431) 2024-05-20 10:48:41 -04:00
Paul
8fcfac61b5 fix(plugin-seo): white screen of death on choosing an existing media for meta image (#6424)
Closes https://github.com/payloadcms/payload/issues/6423

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
2024-05-19 04:51:19 +00:00
Elliot DeNolf
0d544dacdb chore(release): v3.0.0-beta.34 [skip ci] 2024-05-17 16:12:46 -04:00
Alessio Gravili
147b50e719 fix: page metadata generation not working in turbopack (#6417)
In turbo, payloadFaviconDark is a string, not an object with src
2024-05-17 15:44:12 -04:00
720 changed files with 35434 additions and 53529 deletions

50
.github/workflows/label-author.yml vendored Normal file
View File

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

View File

@@ -289,7 +289,8 @@ jobs:
suite:
- _community
- access-control
- admin
- admin__e2e__1
- admin__e2e__2
- auth
- field-error-states
- fields-relationship
@@ -298,7 +299,14 @@ jobs:
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Lexical
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Date
- fields__collections__Number
- fields__collections__Point
- fields__collections__Tabs
- fields__collections__Text
- fields__collections__Upload
- live-preview
- localization
- i18n
@@ -419,8 +427,8 @@ jobs:
cd templates/blank-3.0
cp .env.example .env
ls -la
pnpm add ./*.tgz
pnpm install --ignore-workspace
pnpm add ./*.tgz --ignore-workspace
pnpm install --ignore-workspace --no-frozen-lockfile
cat package.json
pnpm run build

9
.swcrc
View File

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

View File

@@ -16,6 +16,9 @@ export default withBundleAnalyzer(
typescript: {
ignoreBuildErrors: true,
},
experimental: {
reactCompiler: false
},
async redirects() {
return [
{

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"private": true,
"type": "module",
"scripts": {
@@ -102,7 +102,8 @@
"@types/node": "20.12.5",
"@types/prompts": "^2.4.5",
"@types/qs": "6.9.14",
"@types/react": "18.3.2",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"@types/semver": "^7.5.3",
"@types/shelljs": "0.8.15",
"add-stream": "^1.0.0",
@@ -128,11 +129,10 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jwt-decode": "4.0.0",
"lexical": "0.15.0",
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "14.3.0-canary.68",
"next": "15.0.0-rc.0",
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
@@ -144,8 +144,8 @@
"prettier": "^3.0.3",
"prompts": "2.4.2",
"qs": "6.11.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.0.0-rc-f994737d14-20240522",
"react-dom": "^19.0.0-rc-f994737d14-20240522",
"read-stream": "^2.1.1",
"rimraf": "3.0.2",
"semver": "^7.5.4",
@@ -166,8 +166,8 @@
"yocto-queue": "^1.0.0"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
"react": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522",
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
},
"engines": {
"node": ">=18.20.2",
@@ -180,6 +180,8 @@
"uuid": "3.4.0"
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"copyfiles": "$copyfiles",
"cross-env": "$cross-env",
"dotenv": "$dotenv",
@@ -194,6 +196,10 @@
"playwright@1.43.0": "patches/playwright@1.43.0.patch"
}
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
},
"workspaces:": [
"packages/*",
"test/*"

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -41,7 +41,6 @@
"command-exists": "^1.2.9",
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"detect-package-manager": "^3.0.1",
"esprima-next": "^6.0.3",
"execa": "^5.0.0",
"figures": "^6.1.0",

View File

@@ -29,7 +29,7 @@ describe('createProject', () => {
const args = {
_: ['project-name'],
'--db': 'mongodb',
'--local-template': 'blank',
'--local-template': 'blank-3.0',
'--no-deps': true,
} as CliArgs
const packageManager = 'yarn'

View File

@@ -9,7 +9,7 @@ import path from 'path'
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types.js'
import { tryInitRepoAndCommit } from '../utils/git.js'
import { debug, error, warning } from '../utils/log.js'
import { debug, error, info, warning } from '../utils/log.js'
import { configurePayloadConfig } from './configure-payload-config.js'
const filename = fileURLToPath(import.meta.url)
@@ -99,6 +99,7 @@ export async function createProject(args: {
}
if (!cliArgs['--no-deps']) {
info(`Using ${packageManager}.\n`)
spinner.message('Installing dependencies...')
const result = await installDeps({ cliArgs, packageManager, projectDir })
if (result) {

View File

@@ -0,0 +1,29 @@
import commandExists from 'command-exists'
import fse from 'fs-extra'
import type { CliArgs, PackageManager } from '../types.js'
export async function getPackageManager(args: {
cliArgs?: CliArgs
projectDir: string
}): Promise<PackageManager> {
const { cliArgs, projectDir } = args
// Check for yarn.lock, package-lock.json, or pnpm-lock.yaml
let detected: PackageManager = 'npm'
if (
cliArgs?.['--use-pnpm'] ||
fse.existsSync(`${projectDir}/pnpm-lock.yaml`) ||
(await commandExists('pnpm'))
) {
detected = 'pnpm'
} else if (
(cliArgs?.['--use-yarn'] && fse.existsSync(`${projectDir}/yarn.lock`)) ||
(await commandExists('yarn'))
) {
detected = 'yarn'
} else if (cliArgs?.['--use-npm'] && fse.existsSync(`${projectDir}/package-lock.json`)) {
detected = 'npm'
}
return detected || 'npm'
}

View File

@@ -6,24 +6,24 @@ import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { promisify } from 'util'
import type { CliArgs, DbType, NextAppDetails, NextConfigType, PackageManager } from '../types.js'
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { debug as origDebug, warning } from '../utils/log.js'
import { moveMessage } from '../utils/messages.js'
import { installPackages } from './install-packages.js'
import { wrapNextConfig } from './wrap-next-config.js'
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { fileURLToPath } from 'node:url'
import type { CliArgs, DbType, PackageManager } from '../types.js'
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { debug as origDebug, warning } from '../utils/log.js'
import { moveMessage } from '../utils/messages.js'
import { wrapNextConfig } from './wrap-next-config.js'
type InitNextArgs = Pick<CliArgs, '--debug'> & {
dbType: DbType
nextAppDetails?: NextAppDetails
@@ -32,8 +32,6 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
useDistFiles?: boolean
}
type NextConfigType = 'cjs' | 'esm'
type InitNextResult =
| {
isSrcDir: boolean
@@ -55,7 +53,8 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
nextAppDetails.nextAppDir = createdAppDir
}
const { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigType } = nextAppDetails
const { hasTopLevelLayout, isPayloadInstalled, isSrcDir, nextAppDir, nextConfigType } =
nextAppDetails
if (!nextConfigType) {
return {
@@ -222,43 +221,19 @@ function installAndConfigurePayload(
}
async function installDeps(projectDir: string, packageManager: PackageManager, dbType: DbType) {
const packagesToInstall = ['payload', '@payloadcms/next', '@payloadcms/richtext-lexical'].map(
(pkg) => `${pkg}@beta`,
)
const packagesToInstall = [
'payload',
'@payloadcms/next',
'@payloadcms/richtext-lexical',
'@payloadcms/plugin-cloud',
].map((pkg) => `${pkg}@beta`)
packagesToInstall.push(`@payloadcms/db-${dbType}@beta`)
let exitCode = 0
switch (packageManager) {
case 'npm': {
;({ exitCode } = await execa('npm', ['install', '--save', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
case 'yarn':
case 'pnpm': {
;({ exitCode } = await execa(packageManager, ['add', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
case 'bun': {
warning('Bun support is untested.')
;({ exitCode } = await execa('bun', ['add', ...packagesToInstall], { cwd: projectDir }))
break
}
}
// Match graphql version of @payloadcms/next
packagesToInstall.push('graphql@^16.8.1')
return { success: exitCode === 0 }
}
type NextAppDetails = {
hasTopLevelLayout: boolean
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
return await installPackages({ packageManager, packagesToInstall, projectDir })
}
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
@@ -267,6 +242,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
const nextConfigPath: string | undefined = (
await globby('next.config.*js', { absolute: true, cwd: projectDir })
)?.[0]
if (!nextConfigPath || nextConfigPath.length === 0) {
return {
hasTopLevelLayout: false,
@@ -275,6 +251,16 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
}
}
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
if (packageObj.dependencies?.payload) {
return {
hasTopLevelLayout: false,
isPayloadInstalled: true,
isSrcDir,
nextConfigPath,
}
}
let nextAppDir: string | undefined = (
await globby(['**/app'], {
absolute: true,
@@ -288,7 +274,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
nextAppDir = undefined
}
const configType = await getProjectType(projectDir, nextConfigPath)
const configType = getProjectType({ nextConfigPath, packageObj })
const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
@@ -297,7 +283,11 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath, nextConfigType: configType }
}
async function getProjectType(projectDir: string, nextConfigPath: string): Promise<'cjs' | 'esm'> {
function getProjectType(args: {
nextConfigPath: string
packageObj: Record<string, unknown>
}): 'cjs' | 'esm' {
const { nextConfigPath, packageObj } = args
if (nextConfigPath.endsWith('.mjs')) {
return 'esm'
}
@@ -305,7 +295,6 @@ async function getProjectType(projectDir: string, nextConfigPath: string): Promi
return 'cjs'
}
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
const packageJsonType = packageObj.type
if (packageJsonType === 'module') {
return 'esm'

View File

@@ -0,0 +1,42 @@
import execa from 'execa'
import type { PackageManager } from '../types.js'
import { error, warning } from '../utils/log.js'
export async function installPackages(args: {
packageManager: PackageManager
packagesToInstall: string[]
projectDir: string
}) {
const { packageManager, packagesToInstall, projectDir } = args
let exitCode = 0
let stderr = ''
switch (packageManager) {
case 'npm': {
;({ exitCode, stderr } = await execa('npm', ['install', '--save', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
case 'yarn':
case 'pnpm':
case 'bun': {
if (packageManager === 'bun') {
warning('Bun support is untested.')
}
;({ exitCode, stderr } = await execa(packageManager, ['add', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
}
if (exitCode !== 0) {
error(`Unable to install packages. Error: ${stderr}`)
}
return { success: exitCode === 0 }
}

View File

@@ -0,0 +1,89 @@
import execa from 'execa'
import fse from 'fs-extra'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { NextAppDetails } from '../types.js'
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { info } from '../utils/log.js'
import { getPackageManager } from './get-package-manager.js'
import { installPackages } from './install-packages.js'
export async function updatePayloadInProject(
appDetails: NextAppDetails,
): Promise<{ message: string; success: boolean }> {
if (!appDetails.nextConfigPath) return { message: 'No Next.js config found', success: false }
const projectDir = path.dirname(appDetails.nextConfigPath)
const packageObj = (await fse.readJson(path.resolve(projectDir, 'package.json'))) as {
dependencies?: Record<string, string>
}
if (!packageObj?.dependencies) {
throw new Error('No package.json found in this project')
}
const payloadVersion = packageObj.dependencies?.payload
if (!payloadVersion) {
throw new Error('Payload is not installed in this project')
}
const packageManager = await getPackageManager({ projectDir })
// Fetch latest Payload version from npm
const { exitCode: getLatestVersionExitCode, stdout: latestPayloadVersion } = await execa('npm', [
'show',
'payload@beta',
'version',
])
if (getLatestVersionExitCode !== 0) {
throw new Error('Failed to fetch latest Payload version')
}
if (payloadVersion === latestPayloadVersion) {
return { message: `Payload v${payloadVersion} is already up to date.`, success: true }
}
// Update all existing Payload packages
const payloadPackages = Object.keys(packageObj.dependencies).filter((dep) =>
dep.startsWith('@payloadcms/'),
)
const packageNames = ['payload', ...payloadPackages]
const packagesToUpdate = packageNames.map((pkg) => `${pkg}@${latestPayloadVersion}`)
info(`Using ${packageManager}.\n`)
info(
`Updating ${packagesToUpdate.length} Payload packages to v${latestPayloadVersion}...\n\n${packageNames.map((p) => ` - ${p}`).join('\n')}`,
)
const { success: updateSuccess } = await installPackages({
packageManager,
packagesToInstall: packagesToUpdate,
projectDir,
})
if (!updateSuccess) {
throw new Error('Failed to update Payload packages')
}
info('Payload packages updated successfully.')
info(`Updating Payload Next.js files...`)
const templateFilesPath = dirname.endsWith('dist')
? path.resolve(dirname, '../..', 'dist/template')
: path.resolve(dirname, '../../../../templates/blank-3.0')
const templateSrcDir = path.resolve(templateFilesPath, 'src/app/(payload)')
copyRecursiveSync(
templateSrcDir,
path.resolve(projectDir, appDetails.isSrcDir ? 'src/app' : 'app', '(payload)'),
)
return { message: 'Payload updated successfully.', success: true }
}

View File

@@ -2,21 +2,21 @@ import * as p from '@clack/prompts'
import slugify from '@sindresorhus/slugify'
import arg from 'arg'
import chalk from 'chalk'
// @ts-expect-error no types
import { detect } from 'detect-package-manager'
import figures from 'figures'
import path from 'path'
import type { CliArgs, PackageManager } from './types.js'
import type { CliArgs } from './types.js'
import { configurePayloadConfig } from './lib/configure-payload-config.js'
import { createProject } from './lib/create-project.js'
import { generateSecret } from './lib/generate-secret.js'
import { getPackageManager } from './lib/get-package-manager.js'
import { getNextAppDetails, initNext } from './lib/init-next.js'
import { parseProjectName } from './lib/parse-project-name.js'
import { parseTemplate } from './lib/parse-template.js'
import { selectDb } from './lib/select-db.js'
import { getValidTemplates, validateTemplate } from './lib/templates.js'
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
import { writeEnvFile } from './lib/write-env-file.js'
import { error, info } from './utils/log.js'
import {
@@ -85,7 +85,28 @@ export class Main {
// Detect if inside Next.js project
const nextAppDetails = await getNextAppDetails(process.cwd())
const { hasTopLevelLayout, nextAppDir, nextConfigPath } = nextAppDetails
const { hasTopLevelLayout, isPayloadInstalled, nextAppDir, nextConfigPath } = nextAppDetails
// Upgrade Payload in existing project
if (isPayloadInstalled && nextConfigPath) {
p.log.warn(`Payload installation detected in current project.`)
const shouldUpdate = await p.confirm({
initialValue: false,
message: chalk.bold(`Upgrade Payload in this project?`),
})
if (!p.isCancel(shouldUpdate) || shouldUpdate) {
const { message, success: updateSuccess } = await updatePayloadInProject(nextAppDetails)
if (updateSuccess) {
info(message)
} else {
error(message)
}
}
p.outro(feedbackOutro())
process.exit(0)
}
if (nextConfigPath) {
this.args['--name'] = slugify(path.basename(path.dirname(nextConfigPath)))
@@ -96,7 +117,7 @@ export class Main {
? path.dirname(nextConfigPath)
: path.resolve(process.cwd(), slugify(projectName))
const packageManager = await getPackageManager(this.args, projectDir)
const packageManager = await getPackageManager({ cliArgs: this.args, projectDir })
if (nextConfigPath) {
p.log.step(
@@ -212,19 +233,3 @@ export class Main {
}
}
}
async function getPackageManager(args: CliArgs, projectDir: string): Promise<PackageManager> {
let packageManager: PackageManager = 'npm'
if (args['--use-npm']) {
packageManager = 'npm'
} else if (args['--use-yarn']) {
packageManager = 'yarn'
} else if (args['--use-pnpm']) {
packageManager = 'pnpm'
} else {
const detected = await detect({ cwd: projectDir })
packageManager = detected || 'npm'
}
return packageManager
}

View File

@@ -65,3 +65,14 @@ export type DbDetails = {
}
export type EditorType = 'lexical' | 'slate'
export type NextAppDetails = {
hasTopLevelLayout: boolean
isPayloadInstalled?: boolean
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
}
export type NextConfigType = 'cjs' | 'esm'

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -9,7 +9,7 @@ It abstracts all of the email functionality that was in Payload by default in 2.
## Installation
```sh
pnpm add @payloadcms/email-nodemailer` nodemailer
pnpm add @payloadcms/email-nodemailer nodemailer
```
## Usage

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ This adapter allows you to send emails using the [Resend](https://resend.com) RE
## Installation
```sh
pnpm add @payloadcms/email-resend`
pnpm add @payloadcms/email-resend
```
## Usage

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2017 Ivo Meißner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
[Package Link](https://github.com/slicknode/graphql-query-complexity)

View File

@@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) 2016 Jimmy Jia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
[Package Link](https://github.com/taion/graphql-type-json/tree/master)

View File

@@ -1,5 +1,5 @@
import { GraphQLScalarType } from 'graphql'
import { Kind, print } from 'graphql/language'
import { Kind, print } from 'graphql/language/index.js'
function identity(value) {
return value

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "The official live preview React SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -35,12 +35,13 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "18.3.2",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"payload": "workspace:*"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
"react": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522",
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
},
"publishConfig": {
"exports": {
@@ -53,5 +54,9 @@
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
}
}

View File

@@ -6,7 +6,7 @@
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"jsx": "react"
"jsx": "react-jsx"
},
"exclude": [
"dist",

View File

@@ -6,7 +6,7 @@
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"jsx": "react"
"jsx": "react-jsx"
},
"exclude": [
"dist",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -8,6 +8,15 @@
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
},
"experimental": {
"plugins": [
[

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -49,7 +49,7 @@
"@types/busboy": "^1.5.3",
"busboy": "^1.6.0",
"deep-equal": "2.2.2",
"file-type": "19.0.0",
"file-type": "19.0.0 || 19.0.0-rc-f994737d14-20240522",
"graphql-http": "^1.22.0",
"graphql-playground-html": "1.6.30",
"http-status": "1.6.2",
@@ -63,8 +63,8 @@
"devDependencies": {
"@next/eslint-plugin-next": "^14.1.0",
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "18.3.2",
"@types/react-dom": "18.3.0",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"@types/ws": "^8.5.10",
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
@@ -81,7 +81,7 @@
},
"peerDependencies": {
"graphql": "^16.8.1",
"next": "^14.3.0-canary.68",
"next": "^15.0.0-rc.0",
"payload": "workspace:*"
},
"engines": {
@@ -107,5 +107,9 @@
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
}
}

View File

@@ -1,3 +1,4 @@
export { EditView } from '../views/Edit/index.js'
export { DefaultEditView as EditView } from '../views/Edit/Default/index.js'
export { DefaultListView as ListView } from '../views/List/Default/index.js'
export { NotFoundPage } from '../views/NotFound/index.js'
export { type GenerateViewMetadata, RootPage, generatePageMetadata } from '../views/Root/index.js'

View File

@@ -152,7 +152,7 @@ type FetchAPIFileUpload = (args: {
request: Request
}) => Promise<FetchAPIFileUploadResponse>
export const fetchAPIFileUpload: FetchAPIFileUpload = async ({ options, request }) => {
const uploadOptions = { ...DEFAULT_OPTIONS, ...options }
const uploadOptions: FetchAPIFileUploadOptions = { ...DEFAULT_OPTIONS, ...options }
if (!isEligibleRequest(request)) {
debugLog(uploadOptions, 'Request is not eligible for file upload!')
return {

View File

@@ -15,6 +15,7 @@ import 'react-toastify/dist/ReactToastify.css'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
import { DefaultEditView } from '../../views/Edit/Default/index.js'
import { DefaultListView } from '../../views/List/Default/index.js'
@@ -49,6 +50,12 @@ export const RootLayout = async ({
headers,
})
const theme = getRequestTheme({
config,
cookies,
headers,
})
const payload = await getPayloadHMR({ config })
const i18n: I18nClient = await initI18n({
config: config.i18n,
@@ -94,7 +101,7 @@ export const RootLayout = async ({
})
return (
<html className={merriweather.variable} dir={dir} lang={languageCode}>
<html className={merriweather.variable} data-theme={theme} dir={dir} lang={languageCode}>
<body>
<RootProvider
componentMap={componentMap}
@@ -105,6 +112,7 @@ export const RootLayout = async ({
languageOptions={languageOptions}
// eslint-disable-next-line react/jsx-no-bind
switchLanguageServerAction={switchLanguageServerAction}
theme={theme}
translations={i18n.translations}
>
{wrappedChildren}

View File

@@ -6,15 +6,29 @@ import type { BaseRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const access: BaseRouteHandler = async ({ req }) => {
const results = await accessOperation({
const headers = headersWithCors({
headers: new Headers(),
req,
})
return Response.json(results, {
headers: headersWithCors({
headers: new Headers(),
try {
const results = await accessOperation({
req,
}),
status: httpStatus.OK,
})
})
return Response.json(results, {
headers,
status: httpStatus.OK,
})
} catch (e: unknown) {
return Response.json(
{
error: e,
},
{
headers,
status: httpStatus.INTERNAL_SERVER_ERROR,
},
)
}
}

View File

@@ -37,7 +37,9 @@ export const reload = async (config: SanitizedConfig, payload: Payload): Promise
// TODO: support HMR for other props in the future (see payload/src/index init()) hat may change on Payload singleton
await payload.db.init()
await payload.db.connect({ hotReload: true })
if (payload.db.connect) {
await payload.db.connect({ hotReload: true })
}
}
export const getPayloadHMR = async (options: InitOptions): Promise<Payload> => {

View File

@@ -18,17 +18,19 @@ export const getRequestLanguage = ({
}: GetRequestLanguageArgs): AcceptedLanguages => {
const supportedLanguageKeys = <AcceptedLanguages[]>Object.keys(config.i18n.supportedLanguages)
const langCookie = cookies.get(`${config.cookiePrefix || 'payload'}-lng`)
const languageFromCookie: AcceptedLanguages = (
typeof langCookie === 'string' ? langCookie : langCookie?.value
) as AcceptedLanguages
const languageFromHeader = headers.get('Accept-Language')
? extractHeaderLanguage(headers.get('Accept-Language'))
: undefined
if (languageFromCookie && supportedLanguageKeys.includes(languageFromCookie)) {
return languageFromCookie
}
const languageFromHeader = headers.get('Accept-Language')
? extractHeaderLanguage(headers.get('Accept-Language'))
: undefined
if (languageFromHeader && supportedLanguageKeys.includes(languageFromHeader)) {
return languageFromHeader
}

View File

@@ -0,0 +1,33 @@
import type { Theme } from '@payloadcms/ui/providers/Theme'
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js'
import type { SanitizedConfig } from 'payload/config'
import { defaultTheme } from '@payloadcms/ui/providers/Theme'
type GetRequestLanguageArgs = {
config: SanitizedConfig
cookies: Map<string, string> | ReadonlyRequestCookies
headers: Request['headers']
}
const acceptedThemes: Theme[] = ['dark', 'light']
export const getRequestTheme = ({ config, cookies, headers }: GetRequestLanguageArgs): Theme => {
const themeCookie = cookies.get(`${config.cookiePrefix || 'payload'}-theme`)
const themeFromCookie: Theme = (
typeof themeCookie === 'string' ? themeCookie : themeCookie?.value
) as Theme
if (themeFromCookie && acceptedThemes.includes(themeFromCookie)) {
return themeFromCookie
}
const themeFromHeader = headers.get('Sec-CH-Prefers-Color-Scheme') as Theme
if (themeFromHeader && acceptedThemes.includes(themeFromHeader)) {
return themeFromHeader
}
return defaultTheme
}

View File

@@ -52,6 +52,7 @@ export const initPage = async ({
fallbackLocale: null,
locale: locale.code,
req: {
host: headers.get('host'),
i18n,
query: qs.parse(queryString, {
depth: 10,

View File

@@ -30,14 +30,14 @@ export const meta = async (args: MetaConfig & { serverURL: string }): Promise<an
type: 'image/png',
rel: 'icon',
sizes: '32x32',
url: payloadFaviconDark?.src,
url: typeof payloadFaviconDark === 'object' ? payloadFaviconDark?.src : payloadFaviconDark,
},
{
type: 'image/png',
media: '(prefers-color-scheme: dark)',
rel: 'icon',
sizes: '32x32',
url: payloadFaviconLight?.src,
url: typeof payloadFaviconLight === 'object' ? payloadFaviconLight?.src : payloadFaviconLight,
},
]
@@ -79,7 +79,7 @@ export const meta = async (args: MetaConfig & { serverURL: string }): Promise<an
{
alt: ogTitle,
height: 480,
url: staticOGImage.src,
url: typeof staticOGImage === 'object' ? staticOGImage?.src : staticOGImage,
width: 640,
},
],

View File

@@ -9,15 +9,21 @@ import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParam
import { notFound } from 'next/navigation.js'
import React from 'react'
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
import { EditView } from '../Edit/index.js'
import { Settings } from './Settings/index.js'
export { generateAccountMetadata } from './meta.js'
export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, searchParams }) => {
export const Account: React.FC<AdminViewProps> = async ({
initPageResult,
params,
searchParams,
}) => {
const {
locale,
permissions,
req,
req: {
i18n,
payload,
@@ -32,11 +38,17 @@ export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, sear
serverURL,
} = config
const collectionPermissions = permissions?.collections?.[userSlug]
const collectionConfig = config.collections.find((collection) => collection.slug === userSlug)
if (collectionConfig) {
if (collectionConfig && user?.id) {
const { docPermissions, hasPublishPermission, hasSavePermission } =
await getDocumentPermissions({
id: user.id,
collectionConfig,
data: user,
req,
})
const viewComponentProps: ServerSideEditViewProps = {
initPageResult,
params,
@@ -50,9 +62,10 @@ export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, sear
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
collectionSlug={userSlug}
docPermissions={collectionPermissions}
hasSavePermission={collectionPermissions?.update?.permission}
id={user?.id}
docPermissions={docPermissions}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={user?.id.toString()}
isEditing
>
<DocumentHeader

View File

@@ -0,0 +1,41 @@
import type {
Data,
Payload,
PayloadRequest,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
} from 'payload/types'
export const getDocumentData = async (args: {
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
id?: number | string
locale: Locale
payload: Payload
req: PayloadRequest
}): Promise<Data> => {
const { id, collectionConfig, globalConfig, locale, payload, req } = args
let data: Data
if (collectionConfig && id !== undefined && id !== null) {
data = await payload.findByID({
id,
collection: collectionConfig.slug,
depth: 0,
locale: locale.code,
req,
})
}
if (globalConfig) {
data = await payload.findGlobal({
slug: globalConfig.slug,
depth: 0,
locale: locale.code,
req,
})
}
return data
}

View File

@@ -0,0 +1,105 @@
import type { DocumentPermissions } from 'payload/auth'
import type {
Data,
PayloadRequest,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
} from 'payload/types'
import { hasSavePermission as getHasSavePermission } from '@payloadcms/ui/utilities/hasSavePermission'
import { isEditing as getIsEditing } from '@payloadcms/ui/utilities/isEditing'
import { docAccessOperation, docAccessOperationGlobal } from 'payload/operations'
export const getDocumentPermissions = async (args: {
collectionConfig?: SanitizedCollectionConfig
data: Data
globalConfig?: SanitizedGlobalConfig
id?: number | string
req: PayloadRequest
}): Promise<{
docPermissions: DocumentPermissions
hasPublishPermission: boolean
hasSavePermission: boolean
}> => {
const { id, collectionConfig, data = {}, globalConfig, req } = args
let docPermissions: DocumentPermissions
let hasPublishPermission = false
if (collectionConfig) {
try {
docPermissions = await docAccessOperation({
id: id?.toString(),
collection: {
config: collectionConfig,
},
req: {
...req,
data,
},
})
if (collectionConfig.versions?.drafts) {
hasPublishPermission = await docAccessOperation({
id: id?.toString(),
collection: {
config: collectionConfig,
},
req: {
...req,
data: {
...data,
_status: 'published',
},
},
}).then(({ update }) => update?.permission)
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
}
if (globalConfig) {
try {
docPermissions = await docAccessOperationGlobal({
globalConfig,
req: {
...req,
data,
},
})
if (globalConfig.versions?.drafts) {
hasPublishPermission = await docAccessOperationGlobal({
globalConfig,
req: {
...req,
data: {
...data,
_status: 'published',
},
},
}).then(({ update }) => update?.permission)
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
}
const hasSavePermission = getHasSavePermission({
collectionSlug: collectionConfig?.slug,
docPermissions,
globalSlug: globalConfig?.slug,
isEditing: getIsEditing({
id,
collectionSlug: collectionConfig?.slug,
globalSlug: globalConfig?.slug,
}),
})
return {
docPermissions,
hasPublishPermission,
hasSavePermission,
}
}

View File

@@ -1,6 +1,5 @@
import type { EditViewComponent } from 'payload/config'
import type { AdminViewComponent, ServerSideEditViewProps } from 'payload/types'
import type { DocumentPermissions } from 'payload/types'
import type { AdminViewProps } from 'payload/types'
import { DocumentHeader } from '@payloadcms/ui/elements/DocumentHeader'
@@ -9,15 +8,15 @@ import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomCompo
import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo'
import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth'
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
import { hasSavePermission as getHasSavePermission } from '@payloadcms/ui/utilities/hasSavePermission'
import { isEditing as getIsEditing } from '@payloadcms/ui/utilities/isEditing'
import { notFound, redirect } from 'next/navigation.js'
import { docAccessOperation } from 'payload/operations'
import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import { NotFoundView } from '../NotFound/index.js'
import { getDocumentData } from './getDocumentData.js'
import { getDocumentPermissions } from './getDocumentPermissions.js'
import { getMetaBySegment } from './getMetaBySegment.js'
import { getViewsFromConfig } from './getViewsFromConfig.js'
@@ -61,32 +60,33 @@ export const Document: React.FC<AdminViewProps> = async ({
let DefaultView: EditViewComponent
let ErrorView: AdminViewComponent
let docPermissions: DocumentPermissions
let hasSavePermission: boolean
let apiURL: string
let action: string
const data = await getDocumentData({
id,
collectionConfig,
globalConfig,
locale,
payload,
req,
})
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
id,
collectionConfig,
data,
globalConfig,
req,
})
if (collectionConfig) {
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
notFound()
}
try {
docPermissions = await docAccessOperation({
id,
collection: {
config: collectionConfig,
},
req,
})
} catch (error) {
notFound()
}
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
hasSavePermission = getHasSavePermission({ collectionSlug, docPermissions, isEditing })
apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}?locale=${locale.code}${
collectionConfig.versions?.drafts ? '&draft=true' : ''
}`
@@ -117,9 +117,6 @@ export const Document: React.FC<AdminViewProps> = async ({
notFound()
}
docPermissions = permissions?.globals?.[globalSlug]
hasSavePermission = getHasSavePermission({ docPermissions, globalSlug, isEditing })
action = `${serverURL}${apiRoute}/globals/${globalSlug}`
apiURL = `${serverURL}${apiRoute}/${globalSlug}?locale=${locale.code}${
@@ -191,6 +188,7 @@ export const Document: React.FC<AdminViewProps> = async ({
disableActions={false}
docPermissions={docPermissions}
globalSlug={globalConfig?.slug}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={id}
isEditing={isEditing}

View File

@@ -44,10 +44,10 @@ export const DefaultEditView: React.FC = () => {
disableActions,
disableLeaveWithoutSaving,
docPermissions,
getDocPermissions,
getDocPreferences,
getVersions,
globalSlug,
hasPublishPermission,
hasSavePermission,
initialData: data,
initialState,
@@ -115,7 +115,6 @@ export const DefaultEditView: React.FC = () => {
}
void getVersions()
void getDocPermissions()
if (typeof onSaveFromContext === 'function') {
void onSaveFromContext({
@@ -147,7 +146,6 @@ export const DefaultEditView: React.FC = () => {
depth,
collectionSlug,
getVersions,
getDocPermissions,
isEditing,
refreshCookieAsync,
adminRoute,
@@ -221,6 +219,7 @@ export const DefaultEditView: React.FC = () => {
apiURL={apiURL}
data={data}
disableActions={disableActions}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={id}
isEditing={isEditing}

View File

@@ -13,7 +13,7 @@ export interface LivePreviewContextType {
breakpoints: LivePreviewConfig['breakpoints']
fieldSchemaJSON?: ReturnType<typeof fieldSchemaToJSON>
iframeHasLoaded: boolean
iframeRef: React.RefObject<HTMLIFrameElement>
iframeRef: React.RefObject<HTMLIFrameElement | null>
isPopupOpen: boolean
measuredDeviceSize: {
height: number

View File

@@ -32,7 +32,7 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
const { breakpoint, fieldSchemaJSON } = useLivePreviewContext()
const prevWindowType =
React.useRef<ReturnType<typeof useLivePreviewContext>['previewWindowType']>()
React.useRef<ReturnType<typeof useLivePreviewContext>['previewWindowType']>(undefined)
const [fields] = useAllFormFields()

View File

@@ -61,6 +61,7 @@ const PreviewView: React.FC<Props> = ({
docPermissions,
getDocPreferences,
globalSlug,
hasPublishPermission,
hasSavePermission,
initialData,
initialState,
@@ -160,6 +161,7 @@ const PreviewView: React.FC<Props> = ({
apiURL={apiURL}
data={initialData}
disableActions={disableActions}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={id}
isEditing={isEditing}

View File

@@ -2,6 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types'
import { WithServerSideProps } from '@payloadcms/ui/elements/WithServerSideProps'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
import { notFound, redirect } from 'next/navigation.js'
@@ -82,7 +83,16 @@ export const RootPage = async ({
}
const RenderedView = (
<DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} />
<WithServerSideProps
Component={DefaultView}
serverOnlyProps={
{
initPageResult,
params,
searchParams,
} as any
}
/>
)
return (
@@ -100,7 +110,12 @@ export const RootPage = async ({
permissions={initPageResult?.permissions}
searchParams={searchParams}
user={initPageResult?.req.user}
visibleEntities={initPageResult.visibleEntities}
visibleEntities={{
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
// which this caused as soon as initPageResult.visibleEntities is passed in
collections: initPageResult.visibleEntities?.collections,
globals: initPageResult.visibleEntities?.globals,
}}
>
{RenderedView}
</DefaultTemplate>

View File

@@ -17,6 +17,30 @@ export const withPayload = (nextConfig = {}) => {
],
},
},
headers: async () => {
const headersFromConfig = 'headers' in nextConfig ? await nextConfig.headers() : []
return [
...(headersFromConfig || []),
{
source: '/:path*',
headers: [
{
key: 'Accept-CH',
value: 'Sec-CH-Prefers-Color-Scheme',
},
{
key: 'Vary',
value: 'Sec-CH-Prefers-Color-Scheme',
},
{
key: 'Critical-CH',
value: 'Sec-CH-Prefers-Color-Scheme',
},
],
},
]
},
serverExternalPackages: [
...(nextConfig?.serverExternalPackages || []),
'drizzle-kit',

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -108,7 +108,6 @@
"pino-pretty": "10.2.0",
"pluralize": "8.0.0",
"sanitize-filename": "1.6.3",
"scheduler": "0.23.0",
"scmp": "2.1.0",
"ts-essentials": "7.0.3",
"uuid": "^9.0.1"

View File

@@ -1,4 +1,4 @@
import type { I18nClient } from '@payloadcms/translations'
import type { GenericLanguages, I18n, I18nClient } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'
import type React from 'react'
@@ -28,11 +28,12 @@ type RichTextAdapterBase<
}) => Map<string, React.ReactNode>
generateSchemaMap?: (args: {
config: SanitizedConfig
i18n: I18nClient
i18n: I18n<any, any>
schemaMap: Map<string, Field[]>
schemaPath: string
}) => Map<string, Field[]>
hooks?: FieldBase['hooks']
i18n?: Partial<GenericLanguages>
outputSchema?: ({
collectionIDFieldTypes,
config,

View File

@@ -66,7 +66,7 @@ export async function sendVerificationEmail(args: Args): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
email.sendEmail({
from: `"${email.defaultFromName}" <${email.defaultFromName}>`,
from: `"${email.defaultFromName}" <${email.defaultFromAddress}>`,
html,
subject,
to: user.email,

View File

@@ -121,6 +121,7 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
collection,
config,
data,
operation: 'create',
overwriteExistingFiles,
req,
throwOnMissingFile:

View File

@@ -156,6 +156,7 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
collection,
config,
data: bulkUpdateData,
operation: 'update',
overwriteExistingFiles,
req,
throwOnMissingFile: false,

View File

@@ -147,6 +147,7 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
collection,
config,
data,
operation: 'update',
overwriteExistingFiles,
req,
throwOnMissingFile: false,

View File

@@ -17,6 +17,7 @@ import { InvalidConfiguration } from '../errors/index.js'
import { sanitizeGlobals } from '../globals/config/sanitize.js'
import getPreferencesCollection from '../preferences/preferencesCollection.js'
import checkDuplicateCollections from '../utilities/checkDuplicateCollections.js'
import { deepMerge } from '../utilities/deepMerge.js'
import { isPlainObject } from '../utilities/isPlainObject.js'
import { defaults } from './defaults.js'
@@ -154,6 +155,9 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
config: config as SanitizedConfig,
isRoot: true,
})
if (config.editor.i18n && Object.keys(config.editor.i18n).length >= 0) {
config.i18n.translations = deepMerge(config.i18n.translations, config.editor.i18n)
}
}
const promises: Promise<void>[] = []

View File

@@ -9,6 +9,7 @@ import type GraphQL from 'graphql'
import type { Metadata as NextMetadata } from 'next'
import type { DestinationStream, LoggerOptions } from 'pino'
import type React from 'react'
import type { JSX } from 'react'
import type { default as sharp } from 'sharp'
import type { DeepRequired } from 'ts-essentials'

View File

@@ -0,0 +1 @@
export { he } from '@payloadcms/translations/languages/he'

View File

@@ -12,7 +12,7 @@ export { traverseFields as beforeValidateTraverseFields } from '../fields/hooks/
export { formatFilesize } from '../uploads/formatFilesize.js'
export { default as isImage } from '../uploads/isImage.js'
export { isImage } from '../uploads/isImage.js'
export { combineMerge } from '../utilities/combineMerge.js'
export {

View File

@@ -8,6 +8,7 @@ import {
InvalidFieldRelationship,
MissingFieldType,
} from '../../errors/index.js'
import { deepMerge } from '../../utilities/deepMerge.js'
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
import { baseIDField } from '../baseFields/baseIDField.js'
@@ -168,6 +169,10 @@ export const sanitizeFields = async ({
})
}
if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
config.i18n.translations = deepMerge(config.i18n.translations, field.editor.i18n)
}
// Add editor adapter hooks to field hooks
if (!field.hooks) field.hooks = {}

View File

@@ -7,19 +7,6 @@ import type { GeneratedTypes } from '../index.js'
import type { validOperators } from './constants.js'
export type { Payload as Payload } from '../index.js'
export type UploadEdits = {
crop?: {
height?: number
width?: number
x?: number
y?: number
}
focalPoint?: {
x?: number
y?: number
}
}
export type CustomPayloadRequestProperties = {
context: RequestContext
/** The locale that should be used for a field when it is not translated to the requested locale */

View File

@@ -1,3 +1,3 @@
export default function canResizeImage(mimeType: string): boolean {
export function canResizeImage(mimeType: string): boolean {
return ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/tiff'].indexOf(mimeType) > -1
}

View File

@@ -2,7 +2,7 @@ export const percentToPixel = (value, dimension) => {
return Math.floor((parseFloat(value) / 100) * dimension)
}
export default async function cropImage({ cropData, dimensions, file, sharp }) {
export async function cropImage({ cropData, dimensions, file, sharp }) {
try {
const { height, width, x, y } = cropData

View File

@@ -9,22 +9,24 @@ import sanitize from 'sanitize-filename'
import type { Collection } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { PayloadRequestWithData } from '../types/index.js'
import type { FileData, FileToSave, ProbedImageSize } from './types.js'
import type { FileData, FileToSave, ProbedImageSize, UploadEdits } from './types.js'
import { FileRetrievalError, FileUploadError, MissingFile } from '../errors/index.js'
import canResizeImage from './canResizeImage.js'
import cropImage from './cropImage.js'
import { canResizeImage } from './canResizeImage.js'
import { cropImage } from './cropImage.js'
import { getExternalFile } from './getExternalFile.js'
import { getFileByPath } from './getFileByPath.js'
import { getImageSize } from './getImageSize.js'
import getSafeFileName from './getSafeFilename.js'
import resizeAndTransformImageSizes from './imageResizer.js'
import isImage from './isImage.js'
import { getSafeFileName } from './getSafeFilename.js'
import { resizeAndTransformImageSizes } from './imageResizer.js'
import { isImage } from './isImage.js'
type Args<T> = {
collection: Collection
config: SanitizedConfig
data: T
operation: 'create' | 'update'
originalDoc?: T
overwriteExistingFiles?: boolean
req: PayloadRequestWithData
throwOnMissingFile?: boolean
@@ -38,6 +40,8 @@ type Result<T> = Promise<{
export const generateFileData = async <T>({
collection: { config: collectionConfig },
data,
operation,
originalDoc,
overwriteExistingFiles,
req,
throwOnMissingFile,
@@ -53,10 +57,22 @@ export const generateFileData = async <T>({
let file = req.file
const uploadEdits = req.query['uploadEdits'] || {}
const uploadEdits = parseUploadEditsFromReqOrIncomingData({
data,
operation,
originalDoc,
req,
})
const { disableLocalStorage, formatOptions, imageSizes, resizeOptions, staticDir, trimOptions } =
collectionConfig.upload
const {
disableLocalStorage,
focalPoint: focalPointEnabled = true,
formatOptions,
imageSizes,
resizeOptions,
staticDir,
trimOptions,
} = collectionConfig.upload
const staticPath = staticDir
@@ -228,9 +244,9 @@ export const generateFileData = async <T>({
}
}
if (Array.isArray(imageSizes) && fileSupportsResize && sharp) {
if (fileSupportsResize && (Array.isArray(imageSizes) || focalPointEnabled !== false)) {
req.payloadUploadSizes = {}
const { sizeData, sizesToSave } = await resizeAndTransformImageSizes({
const { focalPoint, sizeData, sizesToSave } = await resizeAndTransformImageSizes({
config: collectionConfig,
dimensions: !cropData
? dimensions
@@ -245,13 +261,16 @@ export const generateFileData = async <T>({
savedFilename: fsSafeName || file.name,
sharp,
staticPath,
uploadEdits,
})
fileData.sizes = sizeData
fileData.focalX = focalPoint?.x
fileData.focalY = focalPoint?.y
filesToSave.push(...sizesToSave)
}
} catch (err) {
console.error(err)
req.payload.logger.error(err)
throw new FileUploadError(req.t)
}
@@ -265,3 +284,50 @@ export const generateFileData = async <T>({
files: filesToSave,
}
}
/**
* Parse upload edits from req or incoming data
*/
function parseUploadEditsFromReqOrIncomingData(args: {
data: unknown
operation: 'create' | 'update'
originalDoc: unknown
req: PayloadRequestWithData
}): UploadEdits {
const { data, operation, originalDoc, req } = args
// Get intended focal point change from query string or incoming data
const {
uploadEdits = {},
}: {
uploadEdits?: UploadEdits
} = req.query || {}
if (uploadEdits.focalPoint) return uploadEdits
const incomingData = data as FileData
const origDoc = originalDoc as FileData
// If no change in focal point, return undefined.
// This prevents a refocal operation triggered from admin, because it always sends the focal point.
if (origDoc && incomingData.focalX === origDoc.focalX && incomingData.focalY === origDoc.focalY) {
return undefined
}
if (incomingData?.focalX && incomingData?.focalY) {
uploadEdits.focalPoint = {
x: incomingData.focalX,
y: incomingData.focalY,
}
return uploadEdits
}
// If no focal point is set, default to center
if (operation === 'create') {
uploadEdits.focalPoint = {
x: 50,
y: 50,
}
}
return uploadEdits
}

View File

@@ -149,6 +149,25 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
height,
]
// Add focal point fields if not disabled
if (
uploadOptions.focalPoint !== false ||
uploadOptions.imageSizes ||
uploadOptions.resizeOptions
) {
uploadFields = uploadFields.concat(
['focalX', 'focalY'].map((name) => {
return {
name,
type: 'number',
admin: {
hidden: true,
},
}
}),
)
}
if (uploadOptions.mimeTypes) {
mimeType.validate = mimeTypeValidator(uploadOptions.mimeTypes)
}

View File

@@ -29,7 +29,7 @@ type Args = {
staticPath: string
}
async function getSafeFileName({
export async function getSafeFileName({
collectionSlug,
desiredFilename,
req,
@@ -51,5 +51,3 @@ async function getSafeFileName({
}
return modifiedFilename
}
export default getSafeFileName

View File

@@ -8,8 +8,15 @@ import sanitize from 'sanitize-filename'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SharpDependency } from '../config/types.js'
import type { PayloadRequestWithData, UploadEdits } from '../types/index.js'
import type { FileSize, FileSizes, FileToSave, ImageSize, ProbedImageSize } from './types.js'
import type { PayloadRequestWithData } from '../types/index.js'
import type {
FileSize,
FileSizes,
FileToSave,
ImageSize,
ProbedImageSize,
UploadEdits,
} from './types.js'
import { isNumber } from '../utilities/isNumber.js'
import fileExists from './fileExists.js'
@@ -19,18 +26,16 @@ type ResizeArgs = {
dimensions: ProbedImageSize
file: PayloadRequestWithData['file']
mimeType: string
req: PayloadRequestWithData & {
query?: {
uploadEdits?: UploadEdits
}
}
req: PayloadRequestWithData
savedFilename: string
sharp: SharpDependency
sharp?: SharpDependency
staticPath: string
uploadEdits?: UploadEdits
}
/** Result from resizing and transforming the requested image sizes */
type ImageSizesResult = {
focalPoint?: UploadEdits['focalPoint']
sizeData: FileSizes
sizesToSave: FileToSave[]
}
@@ -71,6 +76,16 @@ const createImageName = (
extension: string,
) => `${outputImageName}-${width}x${height}.${extension}`
type CreateResultArgs = {
filename?: FileSize['filename']
filesize?: FileSize['filesize']
height?: FileSize['height']
mimeType?: FileSize['mimeType']
name: string
sizesToSave?: FileToSave[]
width?: FileSize['width']
}
/**
* Create the result object for the image resize operation based on the
* provided parameters. If the name is not provided, an empty result object
@@ -85,26 +100,28 @@ const createImageName = (
* @param sizesToSave - the sizes to save
* @returns the result object
*/
const createResult = (
name: string,
filename: FileSize['filename'] = null,
width: FileSize['width'] = null,
height: FileSize['height'] = null,
filesize: FileSize['filesize'] = null,
mimeType: FileSize['mimeType'] = null,
sizesToSave: FileToSave[] = [],
): ImageSizesResult => ({
sizeData: {
[name]: {
filename,
filesize,
height,
mimeType,
width,
const createResult = ({
name,
filename = null,
filesize = null,
height = null,
mimeType = null,
sizesToSave = [],
width = null,
}: CreateResultArgs): ImageSizesResult => {
return {
sizeData: {
[name]: {
filename,
filesize,
height,
mimeType,
width,
},
},
},
sizesToSave,
})
sizesToSave,
}
}
/**
* Check if the image needs to be resized according to the requested dimensions
@@ -208,7 +225,7 @@ const sanitizeResizeConfig = (resizeConfig: ImageSize): ImageSize => {
* @param resizeConfig - the resize config
* @returns the result of the resize operation(s)
*/
export default async function resizeAndTransformImageSizes({
export async function resizeAndTransformImageSizes({
config,
dimensions,
file,
@@ -217,10 +234,27 @@ export default async function resizeAndTransformImageSizes({
savedFilename,
sharp,
staticPath,
uploadEdits,
}: ResizeArgs): Promise<ImageSizesResult> {
const { imageSizes } = config.upload
// Noting to resize here so return as early as possible
if (!imageSizes) return { sizeData: {}, sizesToSave: [] }
const { focalPoint: focalPointEnabled = true, imageSizes } = config.upload
// Focal point adjustments
const incomingFocalPoint = uploadEdits.focalPoint
? {
x: isNumber(uploadEdits.focalPoint.x) ? Math.round(uploadEdits.focalPoint.x) : 50,
y: isNumber(uploadEdits.focalPoint.y) ? Math.round(uploadEdits.focalPoint.y) : 50,
}
: undefined
const defaultResult: ImageSizesResult = {
...(focalPointEnabled && incomingFocalPoint && { focalPoint: incomingFocalPoint }),
sizeData: {},
sizesToSave: [],
}
if (!imageSizes || !sharp) {
return defaultResult
}
const sharpBase = sharp(file.tempFilePath || file.data).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
@@ -232,16 +266,13 @@ export default async function resizeAndTransformImageSizes({
// skipped COMPLETELY and thus will not be included in the resulting images.
// All further format/trim options will thus be skipped as well.
if (preventResize(imageResizeConfig, dimensions)) {
return createResult(imageResizeConfig.name)
return createResult({ name: imageResizeConfig.name })
}
const imageToResize = sharpBase.clone()
let resized = imageToResize
if (
req.query?.uploadEdits?.focalPoint &&
applyPayloadAdjustments(imageResizeConfig, dimensions)
) {
if (incomingFocalPoint && applyPayloadAdjustments(imageResizeConfig, dimensions)) {
const { height: resizeHeight, width: resizeWidth } = imageResizeConfig
const resizeAspectRatio = resizeWidth / resizeHeight
const originalAspectRatio = dimensions.width / dimensions.height
@@ -254,27 +285,17 @@ export default async function resizeAndTransformImageSizes({
})
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true })
// Focal point adjustments
const focalPoint = {
x: isNumber(req.query.uploadEdits.focalPoint?.x)
? req.query.uploadEdits.focalPoint.x
: 50,
y: isNumber(req.query.uploadEdits.focalPoint?.y)
? req.query.uploadEdits.focalPoint.y
: 50,
}
const safeResizeWidth = resizeWidth ?? scaledImageInfo.width
const maxOffsetX = scaledImageInfo.width - safeResizeWidth
const leftFocalEdge = Math.round(
scaledImageInfo.width * (focalPoint.x / 100) - safeResizeWidth / 2,
scaledImageInfo.width * (incomingFocalPoint.x / 100) - safeResizeWidth / 2,
)
const safeOffsetX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX)
const safeResizeHeight = resizeHeight ?? scaledImageInfo.height
const maxOffsetY = scaledImageInfo.height - safeResizeHeight
const topFocalEdge = Math.round(
scaledImageInfo.height * (focalPoint.y / 100) - safeResizeHeight / 2,
scaledImageInfo.height * (incomingFocalPoint.y / 100) - safeResizeHeight / 2,
)
const safeOffsetY = Math.min(Math.max(0, topFocalEdge), maxOffsetY)
@@ -306,7 +327,9 @@ export default async function resizeAndTransformImageSizes({
const sanitizedImage = getSanitizedImageData(savedFilename)
req.payloadUploadSizes[imageResizeConfig.name] = bufferData
if (req.payloadUploadSizes) {
req.payloadUploadSizes[imageResizeConfig.name] = bufferData
}
const mimeInfo = await fromBuffer(bufferData)
@@ -327,15 +350,15 @@ export default async function resizeAndTransformImageSizes({
}
const { height, size, width } = bufferInfo
return createResult(
imageResizeConfig.name,
imageNameWithDimensions,
width,
return createResult({
name: imageResizeConfig.name,
filename: imageNameWithDimensions,
filesize: size,
height,
size,
mimeInfo?.mime || mimeType,
[{ buffer: bufferData, path: imagePath }],
)
mimeType: mimeInfo?.mime || mimeType,
sizesToSave: [{ buffer: bufferData, path: imagePath }],
width,
})
}),
)
@@ -345,6 +368,6 @@ export default async function resizeAndTransformImageSizes({
acc.sizesToSave.push(...result.sizesToSave)
return acc
},
{ sizeData: {}, sizesToSave: [] },
{ ...defaultResult },
)
}

View File

@@ -1,4 +1,4 @@
export default function isImage(mimeType: string): boolean {
export function isImage(mimeType: string): boolean {
return (
['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', 'image/avif'].indexOf(
mimeType,

View File

@@ -18,6 +18,8 @@ export type FileSizes = {
export type FileData = {
filename: string
filesize: number
focalX?: number
focalY?: number
height: number
mimeType: string
sizes: FileSizes
@@ -117,3 +119,16 @@ export type FileToSave = {
buffer: Buffer
path: string
}
export type UploadEdits = {
crop?: {
height?: number
width?: number
x?: number
y?: number
}
focalPoint?: {
x?: number
y?: number
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {
@@ -35,7 +35,7 @@
"@aws-sdk/client-s3": "^3.525.0",
"@aws-sdk/credential-providers": "^3.525.0",
"@aws-sdk/lib-storage": "^3.525.0",
"@payloadcms/email-nodemailer": "workspace:^",
"@payloadcms/email-nodemailer": "workspace:*",
"amazon-cognito-identity-js": "^6.1.2",
"nodemailer": "6.9.10",
"resend": "^0.17.2"

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",
@@ -55,7 +55,8 @@
"@payloadcms/eslint-config": "workspace:*",
"@types/escape-html": "^1.0.4",
"@types/express": "^4.17.21",
"@types/react": "18.3.2",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"nodemon": "3.0.3",
@@ -64,8 +65,8 @@
},
"peerDependencies": {
"payload": "workspace:*",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
"react": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522",
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
},
"publishConfig": {
"exports": {
@@ -84,5 +85,9 @@
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"homepage:": "https://payloadcms.com"
"homepage:": "https://payloadcms.com",
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
}
}

View File

@@ -1,4 +1,4 @@
import type { CollectionConfig } from 'payload/types'
import type { CollectionConfig, Field } from 'payload/types'
import type { FormBuilderPluginConfig } from '../../types.js'
@@ -11,6 +11,74 @@ export const generateSubmissionCollection = (
): CollectionConfig => {
const formSlug = formConfig?.formOverrides?.slug || 'forms'
const defaultFields: Field[] = [
{
name: 'form',
type: 'relationship',
admin: {
readOnly: true,
},
relationTo: formSlug,
required: true,
validate: async (value, { req: { payload }, req }) => {
/* Don't run in the client side */
if (!payload) return true
if (payload) {
let _existingForm
try {
_existingForm = await payload.findByID({
id: value,
collection: formSlug,
req,
})
return true
} catch (error) {
return 'Cannot create this submission because this form does not exist.'
}
}
},
},
{
name: 'submissionData',
type: 'array',
admin: {
readOnly: true,
},
fields: [
{
name: 'field',
type: 'text',
required: true,
},
{
name: 'value',
type: 'text',
required: true,
validate: (value: unknown) => {
// TODO:
// create a validation function that dynamically
// relies on the field type and its options as configured.
// How to access sibling data from this field?
// Need the `name` of the field in order to validate it.
// Might not be possible to use this validation function.
// Instead, might need to do all validation in a `beforeValidate` collection hook.
if (typeof value !== 'undefined') {
return true
}
return 'This field is required.'
},
},
],
},
]
const newConfig: CollectionConfig = {
...(formConfig?.formSubmissionOverrides || {}),
slug: formConfig?.formSubmissionOverrides?.slug || 'form-submissions',
@@ -24,74 +92,11 @@ export const generateSubmissionCollection = (
...(formConfig?.formSubmissionOverrides?.admin || {}),
enableRichTextRelationship: false,
},
fields: [
{
name: 'form',
type: 'relationship',
admin: {
readOnly: true,
},
relationTo: formSlug,
required: true,
validate: async (value, { req: { payload }, req }) => {
/* Don't run in the client side */
if (!payload) return true
if (payload) {
let _existingForm
try {
_existingForm = await payload.findByID({
id: value,
collection: formSlug,
req,
})
return true
} catch (error) {
return 'Cannot create this submission because this form does not exist.'
}
}
},
},
{
name: 'submissionData',
type: 'array',
admin: {
readOnly: true,
},
fields: [
{
name: 'field',
type: 'text',
required: true,
},
{
name: 'value',
type: 'text',
required: true,
validate: (value: unknown) => {
// TODO:
// create a validation function that dynamically
// relies on the field type and its options as configured.
// How to access sibling data from this field?
// Need the `name` of the field in order to validate it.
// Might not be possible to use this validation function.
// Instead, might need to do all validation in a `beforeValidate` collection hook.
if (typeof value !== 'undefined') {
return true
}
return 'This field is required.'
},
},
],
},
...(formConfig?.formSubmissionOverrides?.fields || []),
],
fields:
formConfig?.formSubmissionOverrides?.fields &&
typeof formConfig?.formSubmissionOverrides?.fields === 'function'
? formConfig?.formSubmissionOverrides?.fields({ defaultFields })
: defaultFields,
hooks: {
...(formConfig?.formSubmissionOverrides?.hooks || {}),
beforeChange: [

View File

@@ -64,6 +64,159 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col
}
}
const defaultFields: Field[] = [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'fields',
type: 'blocks',
blocks: Object.entries(formConfig?.fields || {})
.map(([fieldKey, fieldConfig]) => {
// let the config enable/disable fields with either boolean values or objects
if (fieldConfig !== false) {
const block = fields[fieldKey]
if (block === undefined && typeof fieldConfig === 'object') {
return fieldConfig
}
if (typeof block === 'object' && typeof fieldConfig === 'object') {
return merge<FieldConfig>(block, fieldConfig, {
arrayMerge: (_, sourceArray) => sourceArray,
})
}
if (typeof block === 'function') {
return block(fieldConfig)
}
return block
}
return null
})
.filter(Boolean) as Block[],
},
{
name: 'submitButtonLabel',
type: 'text',
localized: true,
},
{
name: 'confirmationType',
type: 'radio',
admin: {
description:
'Choose whether to display an on-page message or redirect to a different page after they submit the form.',
layout: 'horizontal',
},
defaultValue: 'message',
options: [
{
label: 'Message',
value: 'message',
},
{
label: 'Redirect',
value: 'redirect',
},
],
},
{
name: 'confirmationMessage',
type: 'richText',
admin: {
condition: (_, siblingData) => siblingData?.confirmationType === 'message',
},
localized: true,
required: true,
},
redirect,
{
name: 'emails',
type: 'array',
admin: {
description:
"Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}.",
},
fields: [
{
type: 'row',
fields: [
{
name: 'emailTo',
type: 'text',
admin: {
placeholder: '"Email Sender" <sender@email.com>',
width: '100%',
},
label: 'Email To',
},
{
name: 'cc',
type: 'text',
admin: {
width: '50%',
},
label: 'CC',
},
{
name: 'bcc',
type: 'text',
admin: {
width: '50%',
},
label: 'BCC',
},
],
},
{
type: 'row',
fields: [
{
name: 'replyTo',
type: 'text',
admin: {
placeholder: '"Reply To" <reply-to@email.com>',
width: '50%',
},
label: 'Reply To',
},
{
name: 'emailFrom',
type: 'text',
admin: {
placeholder: '"Email From" <email-from@email.com>',
width: '50%',
},
label: 'Email From',
},
],
},
{
name: 'subject',
type: 'text',
defaultValue: "You've received a new message.",
label: 'Subject',
localized: true,
required: true,
},
{
name: 'message',
type: 'richText',
admin: {
description: 'Enter the message that should be sent in this email.',
},
label: 'Message',
localized: true,
},
],
},
]
const config: CollectionConfig = {
...(formConfig?.formOverrides || {}),
slug: formConfig?.formOverrides?.slug || 'forms',
@@ -76,159 +229,10 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col
useAsTitle: 'title',
...(formConfig?.formOverrides?.admin || {}),
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'fields',
type: 'blocks',
blocks: Object.entries(formConfig?.fields || {})
.map(([fieldKey, fieldConfig]) => {
// let the config enable/disable fields with either boolean values or objects
if (fieldConfig !== false) {
const block = fields[fieldKey]
if (block === undefined && typeof fieldConfig === 'object') {
return fieldConfig
}
if (typeof block === 'object' && typeof fieldConfig === 'object') {
return merge<FieldConfig>(block, fieldConfig, {
arrayMerge: (_, sourceArray) => sourceArray,
})
}
if (typeof block === 'function') {
return block(fieldConfig)
}
return block
}
return null
})
.filter(Boolean) as Block[],
},
{
name: 'submitButtonLabel',
type: 'text',
localized: true,
},
{
name: 'confirmationType',
type: 'radio',
admin: {
description:
'Choose whether to display an on-page message or redirect to a different page after they submit the form.',
layout: 'horizontal',
},
defaultValue: 'message',
options: [
{
label: 'Message',
value: 'message',
},
{
label: 'Redirect',
value: 'redirect',
},
],
},
{
name: 'confirmationMessage',
type: 'richText',
admin: {
condition: (_, siblingData) => siblingData?.confirmationType === 'message',
},
localized: true,
required: true,
},
redirect,
{
name: 'emails',
type: 'array',
admin: {
description:
"Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}.",
},
fields: [
{
type: 'row',
fields: [
{
name: 'emailTo',
type: 'text',
admin: {
placeholder: '"Email Sender" <sender@email.com>',
width: '100%',
},
label: 'Email To',
},
{
name: 'cc',
type: 'text',
admin: {
width: '50%',
},
label: 'CC',
},
{
name: 'bcc',
type: 'text',
admin: {
width: '50%',
},
label: 'BCC',
},
],
},
{
type: 'row',
fields: [
{
name: 'replyTo',
type: 'text',
admin: {
placeholder: '"Reply To" <reply-to@email.com>',
width: '50%',
},
label: 'Reply To',
},
{
name: 'emailFrom',
type: 'text',
admin: {
placeholder: '"Email From" <email-from@email.com>',
width: '50%',
},
label: 'Email From',
},
],
},
{
name: 'subject',
type: 'text',
defaultValue: "You've received a new message.",
label: 'Subject',
localized: true,
required: true,
},
{
name: 'message',
type: 'richText',
admin: {
description: 'Enter the message that should be sent in this email.',
},
label: 'Message',
localized: true,
},
],
},
...(formConfig?.formOverrides?.fields || []),
],
fields:
formConfig?.formOverrides.fields && typeof formConfig?.formOverrides.fields === 'function'
? formConfig?.formOverrides.fields({ defaultFields })
: defaultFields,
}
return config

View File

@@ -30,20 +30,6 @@ export const formBuilderPlugin =
return {
...config,
// admin: {
// ...config.admin,
// webpack: (webpackConfig) => ({
// ...webpackConfig,
// resolve: {
// ...webpackConfig.resolve,
// alias: {
// ...webpackConfig.resolve.alias,
// [path.resolve(__dirname, 'collections/FormSubmissions/hooks/sendEmail.ts')]: path.resolve(__dirname, 'mocks/serverModule.js'),
// [path.resolve(__dirname, 'collections/FormSubmissions/hooks/createCharge.ts')]: path.resolve(__dirname, 'mocks/serverModule.js'),
// },
// },
// })
// },
collections: [
...(config?.collections || []),
generateFormCollection(formConfig),

View File

@@ -39,12 +39,13 @@ export interface FieldsConfig {
export type BeforeEmail = (emails: FormattedEmail[]) => FormattedEmail[] | Promise<FormattedEmail[]>
export type HandlePayment = (data: any) => void
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
export type FormBuilderPluginConfig = {
beforeEmail?: BeforeEmail
fields?: FieldsConfig
formOverrides?: Partial<CollectionConfig>
formSubmissionOverrides?: Partial<CollectionConfig>
formOverrides?: Partial<Omit<CollectionConfig, 'fields'>> & { fields: FieldsOverride }
formSubmissionOverrides?: Partial<Omit<CollectionConfig, 'fields'>> & { fields: FieldsOverride }
handlePayment?: HandlePayment
redirectRelationships?: string[]
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -6,7 +6,7 @@
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"jsx": "react"
"jsx": "react-jsx"
},
"exclude": [
"dist",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",
@@ -49,7 +49,8 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/express": "^4.17.9",
"@types/react": "18.3.2",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"payload": "workspace:*"
},
"peerDependencies": {
@@ -71,5 +72,9 @@
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"homepage:": "https://payloadcms.com"
"homepage:": "https://payloadcms.com",
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-relationship-object-ids",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
"repository": {
"type": "git",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "Search plugin for Payload",
"keywords": [
"payload",
@@ -50,13 +50,14 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/express": "^4.17.9",
"@types/react": "18.3.2",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "workspace:*",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
"react": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522",
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
},
"publishConfig": {
"exports": {
@@ -75,5 +76,9 @@
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"homepage:": "https://payloadcms.com"
"homepage:": "https://payloadcms.com",
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
}
}

View File

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

View File

@@ -47,7 +47,8 @@
"@types/express": "^4.17.9",
"@types/jest": "^29.5.2",
"@types/node": "20.12.5",
"@types/react": "18.3.2",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
@@ -59,8 +60,8 @@
},
"peerDependencies": {
"payload": "workspace:*",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
"react": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522",
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
},
"publishConfig": {
"exports": {
@@ -73,5 +74,9 @@
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"homepage:": "https://payloadcms.com"
"homepage:": "https://payloadcms.com",
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.37",
"description": "SEO plugin for Payload",
"keywords": [
"payload",
@@ -51,15 +51,16 @@
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/react": "18.3.2",
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2",
"payload": "workspace:*"
},
"peerDependencies": {
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"payload": "workspace:*",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
"react": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522",
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
},
"publishConfig": {
"exports": {
@@ -78,5 +79,9 @@
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"homepage:": "https://payloadcms.com"
"homepage:": "https://payloadcms.com",
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-beta.2"
}
}

View File

@@ -120,7 +120,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
CustomError={errorMessage}
api={api}
collection={collection}
filterOptions={{}}
filterOptions={field.filterOptions}
label={undefined}
onChange={(incomingImage) => {
if (incomingImage !== null) {

View File

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

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