Compare commits

..

48 Commits

Author SHA1 Message Date
Elliot DeNolf
8f6cedf67a chore(release): v3.0.0-beta.47 [skip ci] 2024-06-13 15:36:34 -04:00
Anders Semb Hermansen
7bb2e3be76 feat: adds X-HTTP-Method-Override header (#6487)
Fixes: https://github.com/payloadcms/payload/issues/6486

Adds `X-HTTP-Method-Override` header to allow for sending query params in the body of a POST request. This is useful when the query param string hits the upper limit.
2024-06-13 15:27:39 -04:00
Paul
78db50a497 feat(plugin-stripe): add full req object to stripe webhook handlers (#6770)
Provides `req` to the webhook handlers in Stripe plugin and fixes type
to `PayloadRequest` for req by default.
2024-06-13 19:00:11 +00:00
Jarrod Flesch
f36bf5e4e3 fix: adds translation for authentication:apiKey (#6771)
Fixes https://github.com/payloadcms/payload/issues/6697

Adds `authentication:apiKey` to client translations.
2024-06-13 14:57:58 -04:00
Elliot DeNolf
d10792452f docs: add disclaimer to migration guide 2024-06-13 14:34:51 -04:00
Elliot DeNolf
c500ac83b2 docs: rough draft of migration guide (#6769)
Rough draft of migration guide / breaking changes doc.
2024-06-13 14:23:49 -04:00
Jarrod Flesch
082650c0e2 fix: attempt to use user locale preference when not set as query param (#6761)
Fixes https://github.com/payloadcms/payload/issues/6619

Attempt to use user preference if available when loading view data instead of always relying on query param when loading view data.
2024-06-13 11:22:28 -04:00
Elliot DeNolf
11de4b037d feat!: use Gravatar for default avatar (#6765)
- Fixes #6725 . Gravatar and custom avatar components.
- Makes Gravatar the default
2024-06-13 15:01:44 +00:00
Viet-Tien
0162560996 fix: adds siteName to openGraphSchema joi validation (#6764) 2024-06-13 10:29:32 -04:00
Elliot DeNolf
ed0820f6c8 feat: warn if image resizing enabled but sharp is not passed to config (#6763)
Warning will now show if image resizing enabled, but sharp is not passed
to config.

Fixes #6755
2024-06-13 14:19:57 +00:00
Patrik
e148243260 fix(payload, ui): unable to save animated file types with undefined image sizes (#6757)
## Description

V2 PR [here](https://github.com/payloadcms/payload/pull/6733)

Additionally fixes issue with image thumbnails not updating properly
until page refresh.

Image thumbnails properly update on document save now.

- [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] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-06-13 09:43:44 -04:00
Jacob Fletcher
8e56328e63 fix!: meta.icons type and schema validation (#6759) 2024-06-13 09:36:30 -04:00
Jacob Fletcher
019677b7e6 chore(eslint): consolidates and prevents duplicate imports (#6756)
## Description

Adds ESLint rule to consolidate duplicate imports using the
`import/no-duplicates` rule of the `eslint-plugin-import` plugin. More
here:
https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-duplicates.md.
This was needed as opposed to `no-duplicate-imports` because of the
auto-fix feature.
2024-06-12 16:45:43 -04:00
Elliot DeNolf
0d31021c25 chore(release): v3.0.0-beta.46 [skip ci] 2024-06-12 16:21:26 -04:00
Jarrod Flesch
8e9ed2ebe3 chore: corrects admin.meta joi validation (#6754) 2024-06-12 16:16:23 -04:00
Jessica Chowdhury
763a34f19b fix: corrects block duplicate action and add tests (#6589) 2024-06-12 14:44:17 -04:00
Elliot DeNolf
be0462db56 feat: diff generated types before write (#6749)
Diff types on disk before write
2024-06-12 14:16:03 -04:00
Elliot DeNolf
6e55a2e52d fix: unawaited emails (#6265)
Await email sending, serverless may end before send

Fixes #6457
2024-06-12 14:02:05 -04:00
Alessio Gravili
4e127054ca feat(richtext-lexical)!: sub-field hooks and localization support (#6591)
## BREAKING
- Our internal field hook methods now have new required `schemaPath` and
path `props`. This affects the following functions, if you are using
those: `afterChangeTraverseFields`, `afterReadTraverseFields`,
`beforeChangeTraverseFields`, `beforeValidateTraverseFields`,
`afterReadPromise`
- The afterChange field hook's `value` is now the value AFTER the
previous hooks were run. Previously, this was the original value, which
I believe is a bug
- Only relevant if you have built your own richText adapter: the
richText adapter `populationPromises` property has been renamed to
`graphQLPopulationPromises` and is now only run for graphQL. Previously,
it was run for graphQL AND the rest API. To migrate, use
`hooks.afterRead` to run population for the rest API
- Only relevant if you have built your own lexical features: The
`populationPromises` server feature property has been renamed to
`graphQLPopulationPromises` and is now only run for graphQL. Previously,
it was run for graphQL AND the rest API. To migrate, use
`hooks.afterRead` to run population for the rest API
- Serialized lexical link and upload nodes now have a new `id` property.
While not breaking, localization / hooks will not work for their fields
until you have migrated to that. Re-saving the old document on the new
version will automatically add the `id` property for you. You will also
get a bunch of console logs for every lexical node which is not migrated
2024-06-12 13:33:08 -04:00
Elliot DeNolf
27510bb963 chore(templates): fix vercel one click links [skip ci] 2024-06-11 16:30:11 -04:00
Anders Semb Hermansen
de45e6094b fix(ui): hideGutter was ignored in group field (#6613) 2024-06-11 16:26:00 -04:00
Patryk Kowalczyk
74159de1ec fix: add missing export for useLeaf hook (#6693) 2024-06-11 16:12:25 -04:00
Jarrod Flesch
ba92d864bb fix: list sort preferences (#6731)
Fixes https://github.com/payloadcms/payload/issues/6617

Sets preferences when list sort is set. Uses defaultSort when defined in
config and preferences are not set.
2024-06-11 16:02:28 -04:00
Elliot DeNolf
0fb14cfebe chore(release): v3.0.0-beta.45 [skip ci] 2024-06-11 15:09:41 -04:00
Paul
2ada6fc58d fix: toasts padding and button placement by 1px (#6730)
## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
2024-06-11 18:42:17 +00:00
Alessio Gravili
cb3355b30f feat!: move from react-toastify to sonner (#6682)
**BREAKING:** We now export toast from `sonner` instead of
`react-toastify`. If you send out toasts from your own projects, make
sure to use our `toast` export, or install `sonner`. React-toastify
toasts will no longer work anymore. The Toast APIs are mostly similar,
but there are some differences if you provide options to your toast

CSS styles have been changed from Toastify

```css
/* before */
.Toastify


/* current */
.payload-toast-container
.payload-toast-item
.payload-toast-close-button

/* individual toast items will also have these classes depending on the state */
.toast-info
.toast-warning
.toast-success
.toast-error
```


https://github.com/payloadcms/payload/assets/70709113/da3e732e-aafc-4008-9469-b10f4eb06b35

---------

Co-authored-by: Paul Popus <paul@nouance.io>
2024-06-11 14:12:59 -04:00
Patrik
10c6ffafc3 fix: only use metadata.pages for height if animated (#6728)
## Description

### Issue: 

Non-animated webp / gif files were using `metadata.pages` to calculate
it's resized heights for `imageSizes` or `cropping`.

### Fix: 

It should only use this to calculate it's height if the file's
`metadata` contains `metadata.pages`. Non-animated webps and gifs would
not have this.

- [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-06-11 13:45:49 -04:00
Patrik
6512d5ce69 fix: create sharp file for fileHasAdjustments files or fileIsAnimated files (#6708)
## Description

Fixes #6694 

Previously we were only creating sharp files for files that have file
adjustments but instead a sharp file should be created for animated
images even if there are no file adjustments - i.e

`const fileHasAdjustments = fileSupportsResize && Boolean(resizeOptions
|| formatOptions || imageSizes || trimOptions || file.tempFilePath)`

- [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-06-11 10:55:51 -04:00
Jarrod Flesch
57fcc9148e fix: corrects field-paths that were incorrectly being set (#6724)
Fixes https://github.com/payloadcms/payload/issues/6650

Similar to [6712](https://github.com/payloadcms/payload/pull/6712). Field paths were
not accounting for the 4 scenarios:
- both parentPath & fieldName
- only parentPath
- only fieldName
- neither parentPath or fieldName (top level rows, etc)
2024-06-11 10:17:40 -04:00
Elliot DeNolf
36f4f23463 chore(release): v3.0.0-beta.44 [skip ci] 2024-06-11 09:46:31 -04:00
Alessio Gravili
7b7dc71845 fix: get auto type-gen to work on turbo, by running type gen in a child process outside turbo/webpack (#6714)
Before on turbo: https://github.com/vercel/next.js/issues/66723
2024-06-10 22:03:12 +00:00
Jarrod Flesch
ba513d5a97 fix: corrects tab paths when nested within other row like fields (#6712)
Fixes https://github.com/payloadcms/payload/issues/6637

There was an issue where tab paths were being generated based on 2
scenarios when there are 3 possible scenarios:
- A path is provided and the tab is named
- A path is **not** provided but the tab is named
- Neither a path or a tab name are provided
2024-06-10 16:06:09 -04:00
Jarrod Flesch
a26d03190e fix: re-exports graphql json types for external use (#6711)
Fixes https://github.com/payloadcms/payload/issues/6683

Exports import `GraphQLJSON` and `GraphQLJSONObject` from
`@payloadcms/graphql/types`

```ts
import { GraphQLJSON, GraphQLJSONObject } from '@payloadcms/graphql/types'
```
2024-06-10 14:53:31 -04:00
Patrik
9f525621c8 fix(ui): removes array & blocks & group fields from sort (#6576)
## Description

V2 PR [here](https://github.com/payloadcms/payload/pull/6574)

- [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-06-10 14:09:50 -04:00
Elliot DeNolf
7309d474ee feat!: type auto-generation (#6657)
Types are now auto-generated by default.

You can opt-out of this behavior by setting:
```ts
buildConfig({
  // Rest of config
  typescript: {
    autoGenerate: false
  },
})
```
2024-06-10 13:42:44 -04:00
Jarrod Flesch
45e86832c2 fix: global draft validations (#6709)
- Extends draft validation from https://github.com/payloadcms/payload/pull/6677 to work with globals as
well

- Fixes bug from https://github.com/payloadcms/payload/pull/6677 where
autosave was not saving properly after first autosave
2024-06-10 12:31:22 -04:00
Alessio Gravili
1bd91b23ca chore: improved clean commands which work on windows and output pretty summary (#6685) 2024-06-09 05:21:11 +00:00
Alessio Gravili
ac34380eb8 fix(ui): set checkbox htmlFor by default, fixing some checkbox labels not toggling the checkbox (#6684) 2024-06-08 19:34:26 +00:00
Jacob Fletcher
17707852e0 chore: migrates @faceless-ui imports to esm (#6681) 2024-06-07 22:59:39 -04:00
Elliot DeNolf
8b95218577 chore(release): v3.0.0-beta.43 [skip ci] 2024-06-07 17:45:28 -04:00
Jarrod Flesch
a79d23c631 chore: adjusts test config for draft validation (#6678) 2024-06-07 16:01:03 -04:00
Jarrod Flesch
52c81ad525 feat: adds draft validation option (#6677)
## Description

Allows draft validation to be enabled at the config level.

You can enable this by:
```ts
// ...collectionConfig
versions: {
  drafts: {
    validate: true // defaults to false
  }
}
```
2024-06-07 15:22:03 -04:00
Paul
8ec836737e chore: add turbo resolveAlias mock alias to hide webpack warnings (#6676) 2024-06-07 17:23:35 +00:00
Paul
e4a90294ea feat(plugin-redirects)!: update fields overrides to use a function (#6675)
## Description

Updates the `fields` override in plugin redirects to allow for
overriding

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

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


## Type of change

- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
2024-06-07 14:41:09 +00:00
Jacob Fletcher
7c8d562f03 fix(next): live preview device position when using zoom (#6665) 2024-06-07 10:17:49 -04:00
Alessio Gravili
11c3a65e63 feat(richtext-*): allow omitting the root editor property (#6660)
No need to add lexical/slate to the bundle if someone decides not to
make use of richText fields within payload at all
2024-06-06 17:57:03 +00:00
Paul
8dd5e4dc24 fix: max versions config not being respected on globals (#6654)
Closes https://github.com/payloadcms/payload/issues/6646

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
2024-06-06 17:21:32 +00:00
Alessio Gravili
9bd9e7a986 feat!: upgrade minimum node 20 version from 20.6.0 to 20.9.0 (#6659)
**BREAKING**:
- This bumps the minimum required node version from node 20.6.0 to node
20.9.0. This is because 20.6.0 breaks type generation due to a CJS node
bug, and 20.9.0 is the next v20 LTS version. The minimum node 18 version
stays the same (18.20.2)
2024-06-06 17:15:21 +00:00
420 changed files with 6541 additions and 1661 deletions

7
.vscode/launch.json vendored
View File

@@ -111,6 +111,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js field-error-states",
"cwd": "${workspaceFolder}",
"name": "Run Dev Field Error States",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run test:int live-preview",
"cwd": "${workspaceFolder}",

View File

@@ -0,0 +1,804 @@
# 🚧 **DRAFT:** 3.0 Migration Guide / Breaking Changes
> [!IMPORTANT]
> This document will continue to be updated and cleaned up until the 3.0 release.
## What has changed?
The core logic and principles of Payload remain the same for 3.0. The brunt of the changes are at the HTTP layer and the Admin Panel. These aspects were moved to be based upon Next.js.
## To migrate from Payload 2.0 to 3.0:
1. Delete the `admin.bundler` property from your Payload config:
Payload no longer bundles the admin panel. Instead, we rely directly on Next.js for bundling. This also means that the `@payloadcms/bundler-webpack` and `@payloadcms/bundler-vite` packages have been deprecated. You can completely uninstall those from your project by removing them from your `package.json` file and re-running your package managers installation process, i.e. `pnpm i`.
2. Add the `secret` property to your Payload config. This used to be set in the `payload.init()` function of your `server.ts` file. Move it to `payload.config.ts` instead:
```tsx
// payload.config.ts
buildConfig({
// ...
secret: process.env.PAYLOAD_SECRET
})
```
3. The `admin.css` and `admin.scss` properties in the Payload config have been removed:
Instead for any global styles you can:
- use `(payload)/custom.scss` to import or add your own styles, eg. for tailwind
- plugins that need to inject global styles should do so via the provider config at `admin.components.providers` :
```tsx
// payload.config.js
//...
admin: {
components: {
providers: [
MyProvider
]
}
},
//...
// MyProvider.tsx
import React from 'react'
import './globals.css'
const MyProvider: React.FC<{children?: any}= ({ children }) ={
return (
<React.fragment>
{children}
</React.fragment>
)
}
export default Provider
```
4. The `admin.indexHTML` property has been removed.
5. The `collection.admin.hooks` property has been removed.
Instead, use the new `beforeDuplicate` field-level hook which take the usual field hook arguments.
```tsx
// TODO: add snippet here of old vs new
```
6. Import all Payload React components via the `@payloadcms/ui` package instead of `payload`:
If you were previously importing components into your app from the `payload` package, for example to create a custom component, you will need to:
- Change your import paths (see below)
- Migrate your component (if necessary, see next bullet)
```tsx
import { Button } from '@payloadcms/ui/elements/Button'
// TODO: Add new full list of exports
```
7. Migrate all Custom Components to Server Components.
All Custom Components are now server-rendered, and therefore, cannot use state or hooks directly. If youre using Custom Components in your app that requires state or hooks, define your component in a separate file with the `'use client'` directive at the top, then render *that* client component within your server component. Remember you can only pass serializable props to this component, so props cannot be blindly spread.
```tsx
import React from 'react'
import type { ServerOnlyProps } from './types.ts'
import MyClientComponent from './client.tsx'
export const MyServerComponent: React.FC<ServerOnlyProps= (serverOnlyProps) ={
const clientOnlyProps = {
// ... sanitize server-only props here as needed
}
return (
<MyClientComponent {...clientOnlyProps} />
)
}
```
8. The `custom` property in the Payload config, i.e. collections, globals, blocks, and fields are now ***server only***.
Use `admin.custom` properties will be available in both server and client bundles.
```tsx
// payload.config.ts
buildConfig({
// Server Only
custom: {
someProperty: 'value'
},
admin: {
custom: {
name: 'Customer portal' // Available in server and client
}
},
})
```
9. The `admin.description` property on field configs no longer attaches `data` to its args:
This is because we cannot pass your `description` function to the client for execution (functions are not serializable, and state is held on the client). To display dynamic descriptions that use current `data` or `path`, you must now pass a custom component and subscribe to the fields state yourself using Payloads React hooks:
```tsx
// TODO: add config snippet for total clarity
import React from 'react'
// TODO: get rest of imports
export const CustomFieldDescriptionComponent: DescriptionComponent = () ={
const { path } = useFieldProps()
const { value } = useFormFields(([fields]) =fields[path])
return (
<div>
{`Component description: ${path} - ${value}`}
</div>
)
}
```
10. The `admin.label` property on the `collapsible` field no longer attaches `data` to its args.
This is because we cannot pass your `label` function to the client for execution (functions are not serializable, and state is held on the client). To display dynamic labels that use current `data` or `path`, you must now pass a custom component and subscribe to the fields state yourself using Payloads React hooks:
```tsx
// TODO: add config snippet for total clarity
import React from 'react'
// TODO: get rest of imports
export const CustomFieldLabelComponent: LabelComponent = () => {
const { path } = useFieldProps()
const { value } = useFormFields(([fields]) =fields[path])
return (
<div>
{`Component label: ${path} - ${value}`}
</div>
)
}
```
11. The `admin.components.Cell` no longer receives `rowData` or `cellData`.
If using a custom component, you must now get the data yourself via the `useTableCell` hook, i.e. `const { cellData, rowData } = useTableCell()`.
```tsx
// TODO: add config snippet for total clarity
import React from 'react'
// TODO: get rest of imports
export const CustomCellComponent: CellComponent = () ={
const { cellData, rowData } = useTableCell()
return (
<div>
{`Component cell: ${cellData}`}
</div>
)
}
```
12. `admin.components.RowLabel` no longer accepts a function, instead use a custom component and make use of the `useRowLabel` hook:
```tsx
// ❌ Before
// Field config
{
type: 'array',
admin: {
components: {
RowLabel: ({ data }) ={
console.log(data)
return data?.title || 'Untitled'
},
}
}
}
```
```tsx
// ✅ After
// Field config
{
type: 'array',
admin: {
components: {
RowLabel: ArrayRowLabel
}
}
}
// Custom Component
'use client'
import type { RowLabelComponent } from 'payload/types'
import { useRowLabel } from '@payloadcms/ui/forms/RowLabel/Context'
import React from 'react'
export const ArrayRowLabel: RowLabelComponent = () ={
const { data } = useRowLabel<{ title: string }>()
return (
<div>{data.title || 'Untitled'}</div>
)
}
```
13. The `admin.components.views[].Tab.pillLabel` has been replaced with `admin.components.views[].Tab.Pill`
```tsx
// Collection.ts
// ❌ Before
{
admin: {
components: {
views: {
Edit: {
Tab: {
pillLabel: '',
},
},
},
},
},
}
// ✅ After
{
admin: {
components: {
views: {
Edit: {
Tab: {
Pill: MyPill,
},
},
},
},
},
}
```
14. The `useTitle` hook has been absorbed by the `useDocumentInfo` hook.
Now, you can get title directly from document info context, i.e. `const { title } = useDocumentInfo()`.
15. The `Fields` type was renamed to `FormState`:
This was changed for improved semantics. If you were previously importing this type in your own application, simply change the import name:
```tsx
// ❌ Before
import type { Fields } from 'payload/types'
// ✅ After
import type { FormState } from 'payload/types'
```
16. The `useDocumentInfo` hook no longer returns a `SanizitedCollectionConfig` or `SanitizedGlobalConfig`:
This is because the configs themselves are not serializable and so they cannot be thread through to the client, i.e. the `DocumentInfoContext`. Instead, various properties of the config are passed instead, like `collectionSlug` and `globalSlug`. You can use these to access a client-side config, if needed, through the `useConfig` hook (see next bullet).
17. The `useConfig` hook now returns a `ClientConfig` and not a `SanizitedConfig`.
This is because the config itself is not serializable and so it is not able to be thread through to the client, i.e. the `ConfigContext`.
18. `DocumentTabProps` no longer contains `id` or `isEditing`.
Instead you can use the `useDocumentInfo` hook to get this information (see above).
19. The args of the `livePreview.url` function have changed.
It no longer receives `documentInfo` as an arg, and instead, now has `collectionConfig` and `globalConfig`.
20. The `href` and `isActive` functions in the view tab config no longer sends the `match` or `location` arguments.
This is is a property specific to React Router, not Next.js. If you need to do fancy matching similar to this, use a custom tab that fires of some hooks, i.e. `usePathname()` and run it through your own utility functions.
21. The `views.Edit` or `views.Edit.Default` or `views.Edit.Default.Component` properties are no longer of type `AdminViewComponent` like the other views.
Instead, their new type is `React.FC<EditViewProps>` where you now only receive the config slug. This is because of how custom edit views need to be rendered server-side, then returned by a client-component (i.e. the document drawer). Theres an example of this adapter pattern in the first sections of this page.
22. `beforeDuplicate` field hooks have been added to `unique` fields to automatically add “- Copy” to the end.
23. The `useCollapsible` hook has had slight changes to its property names:
`collapsed` is now `isCollapsed` and `withinCollapsible` is now `isWithinCollapsible`.
24. Components that return a function have webpack errors.
Will need to document the following (if intended as a breaking change)
![Untitled](https://prod-files-secure.s3.us-west-2.amazonaws.com/0fcec415-321b-48ca-a915-504d61c448b3/94156826-74ee-4708-aa73-1beb11ad0306/Untitled.png)
25. The `admin.favicon` property is now `admin.icons` and the types have changed
Reference: https://github.com/payloadcms/payload/pull/6347
```tsx
// payload.config.ts
// ❌ Before
{
// ...
admin: {
favicon: 'path-to-favicon.svg'
}
}
// ✅ After
{
// ...
admin: {
icons: [{
path: 'path-to-favicon.svg',
sizes: '32x32'
}]
}
}
```
See also: https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons
26. The `admin.meta.ogImage` property has been replaced by `admin.meta.openGraph.images`
Reference: https://github.com/payloadcms/payload/pull/6227
```tsx
// payload.config.ts
// ❌ Before
{
admin: {
meta: {
ogImage: ''
}
}
}
```
```tsx
// ✅ After
{
admin: {
meta: {
openGraph: {
images: []
}
}
}
}
```
See also : https://nextjs.org/docs/app/api-reference/functions/generate-metadata#opengraph
27. The `admin.logoutRoute` and `admin.inactivityRoute` properties have been consolidated into a single `admin.routes` property
Reference: https://github.com/payloadcms/payload/pull/6354
To migrate, simply move those two keys as follows:
```tsx
// payload.config.ts
// ❌ Before
{
// ...
admin: {
logoutRoute: '/custom-logout',
inactivityRoute: '/custom-inactivity'
}
}
// ✅ After
{
// ...
admin: {
routes: {
logout: '/custom-logout',
inactivity: '/custom-inactivity'
}
}
}
```
## Environment variables
- Environment variables prefixed with `PAYLOAD_PUBLIC` will no longer be available on the client. In order to access them on the client, those will now have to be prefixed with `NEXT_PUBLIC` instead
## i18n
- `useTranslation()` hook no longer takes any options, any translations using shorthand accessors will need to use the entire `group:key`
```tsx
// ❌ Before
const { i18n, t } = useTranslation('general')
return <p>{t('cancel')}</p>
// ✅ After
const { i18n, t } = useTranslation()
return <p>{t('general:cancel')}</p>
```
- `react-i18n` was removed, the `Trans` component from react-i18n has been replaced with a payload provided solution. You can instead `import { Translation } from "@payloadcms/ui"`
```tsx
// Translation string example
// "loggedInChangePassword": "To change your password, go to your <0>account</0> and edit your password there."
// ❌ Before
<Trans i18nKey="loggedInChangePassword" t={t}>
<Link to={`${admin}/account`}>account</Link>
</Trans>
// ✅ After
<Translation
t={t}
i18nKey="authentication:loggedInChangePassword"
elements={{
'0': ({ children }) => <Link href={`${admin}/account`} children={children} />,
}}
/>
```
## Description and Label handling
https://github.com/payloadcms/payload/pull/6264
- Globals config: `admin.description` no longer accepts a custom component. You will have to move it to `admin.components.elements.Description`
- Collections config: `admin.description` no longer accepts a custom component. You will have to move it to `admin.components.edit.Description`
- All Fields: `field.admin.description` no longer accepts a custom component. You will have to move it to `field.admin.components.Description`
- Collapsible Field: `field.label` no longer accepts a custom component. You will have to move it to `field.admin.components.RowLabel`
- Array Field: `field.admin.components.RowLabel` no longer accepts strings or records
- If you are using our exported field components in your own app, their `labelProps` property has been stripped down and no longer contains the `label` and `required` prop. Those can now only be configured at the top-level.
## Custom Endpoints
- `root` endpoints no longer exist on the config. If you want to create a “root” endpoint, you can add them to the api folder within your Payload application. See Next docs: https://nextjs.org/docs/app/api-reference/file-conventions/route
- Endpoint handlers
- arguments
- ❌ Before: `(req, res, next)`
- ✅ After: `(req)`
- return
- ❌ Before: `res.json`, `res.send`, etc.
- ✅ After: a valid HTTP [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
```tsx
// ❌ Before
{
path: '/whoami/:parameter',
method: 'post',
handler: (req, res) => {
res.json({
parameter: req.params.parameter,
name: req.body.name,
age: req.body.age,
})
}
}
// ✅ After
{
path: '/whoami/:parameter',
method: 'post',
handler: (req) => {
return Response.json({
parameter: req.routeParams.parameter,
// ^^ `params` is now `routeParams`
name: req.data.name,
age: req.data.age,
})
}
}
```
- Handlers no longer resolve `data` for you on the request, use `req.json()` or you can use our utilities
```tsx
// ❌ Before
{
path: '/whoami/:parameter',
method: 'post',
handler: async (req) => {
return Response.json({
name: req.data.name, // data will be undefined
})
}
}
// ✅ After
import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
{
path: '/whoami/:parameter',
method: 'post',
handler: async (req) => {
// mutates req, must be awaited
await addDataAndFileToRequest(req)
return Response.json({
name: req.data.name, // data is now available
})
}
}
```
- Handlers no longer resolve `locale` and `fallbackLocale` for you
```tsx
// ❌ Before
{
path: '/whoami/:parameter',
method: 'post',
handler: async (req) => {
return Response.json({
// will be undefined
fallbackLocale: req.fallbackLocale,
locale: req.locale,
})
}
}
// ✅ After
import { addLocalesToRequest } from '@payloadcms/next/utilities'
{
path: '/whoami/:parameter',
method: 'post',
handler: async (req) => {
// mutates req
addLocalesToRequest(req)
return Response.json({
fallbackLocale: req.fallbackLocale,
locale: req.locale,
})
}
}
```
## Req (Hooks, Access-control, etc)
- The `req` used to extend the Express Request, now it extends the [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). So you may need to change things in your code, for example if you are relying on `req.headers['content-type']` you will now need to to use `req.headers.get('content-type')`
## Uploads
- `staticDir` must now be an absolute path. Before it would attempt to use the location of the payload config and merge the relative path set for staticDir.
- `staticURL` has been removed. If you were using this format URLs when using an external provider, you can leverage the `generateFileURL` functions in order to do the same.
## Email Adapters
Email functionality has been abstracted out into email adapters.
- All existing nodemailer functionality was abstracted into the `@payloadcms/email-nodemailer` package
- No longer configured with ethereal.email by default.
- Ability to pass email into the `init` function has been removed.
- Warning will be given on startup if email not configured. Any `sendEmail` call will simply log the To address and subject.
- A Resend adapter is now also available via the `@payloadcms/email-resend` package.
### If you used the default email configuration in 2.0 (nodemailer):
```tsx
// ❌ Before
// via payload.init
payload.init({
email: {
transport: someNodemailerTransport
fromName: 'hello',
fromAddress: 'hello@example.com',
},
})
// or via email in payload.config.ts
export default buildConfig({
email: {
transport: someNodemailerTransport
fromName: 'hello',
fromAddress: 'hello@example.com',
},
})
// ✅ After
// Using new nodemailer adapter package
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
export default buildConfig({
email: nodemailerAdapter() // This will be the old ethereal.email functionality
})
// or pass in transport
export default buildConfig({
email: nodemailerAdapter({
defaultFromAddress: 'info@payloadcms.com',
defaultFromName: 'Payload',
transport: await nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
})
})
```
### Removal of rate-limiting
- Now only available if using custom server and using express or similar
# Plugins
- *All* plugins have been standardized to use *named exports* (as opposed to default exports). Most also have a suffix of `Plugin` to make it clear what is being imported.
```tsx
// ❌ Before
import seo from '@payloadcms/plugin-seo'
import stripePlugin from '@payloadcms/plugin-stripe'
// ✅ After
import { seoPlugin } from '@payloadcms/plugin-seo'
import { stripePlugin } from '@payloadcms/plugin-stripe'
// etc.
```
## `@payloadcms/plugin-cloud-storage`
- The adapters that are exported from `@payloadcms/plugin-cloud-storage` (ie. `@payloadcms/plugin-cloud-storage/s3`) package have been removed.
- New *standalone* packages have been created for each of the existing adapters. Please see the documentation for the one that you use.
- `@payloadcms/plugin-cloud-storage` is still fully supported but should only to be used if you are providing a custom adapter that does not have a dedicated package.
- If you have created a custom adapter, the type must now provide a `name` property.
| Service | Package |
| -------------------- | ---------------------------------------------------------------------------- |
| Vercel Blob | https://github.com/payloadcms/payload/tree/beta/packages/storage-vercel-blob |
| AWS S3 | https://github.com/payloadcms/payload/tree/beta/packages/storage-s3 |
| Azure | https://github.com/payloadcms/payload/tree/beta/packages/storage-azure |
| Google Cloud Storage | https://github.com/payloadcms/payload/tree/beta/packages/storage-gcs |
```tsx
// ❌ Before (required peer dependencies depending on adapter)
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
import { s3Adapter } from '@payloadcms/plugin-cloud-storage/s3'
plugins: [
cloudStorage({
collections: {
[mediaSlug]: {
adapter: s3Adapter({
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
region: process.env.S3_REGION,
},
}),
},
},
}),
],
// ✅ After
import { s3Storage } from '@payloadcms/storage-s3'
plugins: [
s3Storage({
collections: {
[mediaSlug]: true,
},
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
region: process.env.S3_REGION,
},
}),
],
```
## `@payloadcms/plugin-form-builder`
- Field overrides for form and form submission collections now accept a function with a `defaultFields` inside the args instead of an array of config
```tsx
// ❌ Before
fields: [
{
name: 'custom',
type: 'text',
}
]
// ✅ After
fields: ({ defaultFields }) => {
return [
...defaultFields,
{
name: 'custom',
type: 'text',
},
]
}
```
## `@payloadcms/plugin-redirects`
- Field overrides for the redirects collection now accepts a function with a `defaultFields` inside the args instead of an array of config
```tsx
// ❌ Before
fields: [
{
name: 'custom',
type: 'text',
}
]
// ✅ After
fields: ({ defaultFields }) => {
return [
...defaultFields,
{
name: 'custom',
type: 'text',
},
]
}
```
## `@payloadcms/richtext-lexical`
// TODO: Needs comprehensive breaking changes / migration steps
### Custom Features
- Previously, a Feature would contain both server code (e.g. population promises) and client code (e.g. toolbar items). Now, they have been split up into server features and client features

View File

@@ -618,3 +618,45 @@ export const Orders: CollectionConfig = {
**req** will have the **payload** object and can be used inside your endpoint handlers for making
calls like req.payload.find() that will make use of access control and hooks.
</Banner>
## Method Override for GET Requests
Payload supports a method override feature that allows you to send GET requests using the HTTP POST method. This can be particularly useful in scenarios when the query string in a regular GET request is too long.
### How to Use
To use this feature, include the `X-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`.
### Example
Here is an example of how to use the method override to perform a GET request:
#### Using Method Override (POST)
```ts
const res = await fetch(`${api}/${collectionSlug}`, {
method: 'POST',
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/x-www-form-urlencoded',
'X-HTTP-Method-Override': 'GET',
},
body: qs.stringify({
depth: 1,
locale: 'en',
}),
})
```
#### Equivalent Regular GET Request
```ts
const res = await fetch(`${api}/${collectionSlug}?depth=1&locale=en`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
})
```

View File

@@ -22,6 +22,7 @@ Collections and Globals both support the same options for configuring drafts. Yo
| Draft Option | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](/docs/versions/autosave). |
| `validate` | Set `validate` to `true` to validate draft documents when saved. Default is `false`. |
## Database changes

View File

@@ -33,6 +33,7 @@ export default withBundleAnalyzer(
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
}
return webpackConfig
},
}),

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.47",
"private": true,
"type": "module",
"scripts": {
@@ -9,6 +9,7 @@
"build:app": "next build",
"build:app:analyze": "cross-env ANALYZE=true next build",
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\"",
"build:core:force": "pnpm clean:build && turbo build --filter \"!@payloadcms/plugin-*\" --no-cache --force",
"build:create-payload-app": "turbo build --filter create-payload-app",
"build:db-mongodb": "turbo build --filter db-mongodb",
"build:db-postgres": "turbo build --filter db-postgres",
@@ -42,21 +43,21 @@
"build:translations": "turbo build --filter translations",
"build:ui": "turbo build --filter ui",
"clean": "turbo clean",
"clean:all": "find . \\( -type d \\( -name node_modules -o -name dist -o -name .cache -o -name .next -o -name .turbo \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} +",
"clean:build": "find . \\( -type d \\( -name dist -o -name .cache -o -name .next -o -name .turbo \\) -o -type f -name tsconfig.tsbuildinfo \\) -not -path '*/node_modules/*' -exec rm -rf {} +",
"clean:cache": "rimraf node_modules/.cache && rimraf packages/payload/node_modules/.cache && rimraf .next",
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media' '**/dist' '**/.cache' '**/.next' '**/.turbo' '**/tsconfig.tsbuildinfo' '**/payload*.tgz'",
"clean:build": "node ./scripts/delete-recursively.js 'media' '**/dist' '**/.cache' '**/.next' '**/.turbo' '**/tsconfig.tsbuildinfo' '**/payload*.tgz'",
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next",
"dev": "cross-env NODE_OPTIONS=--no-deprecation node ./test/dev.js",
"dev:generate-graphql-schema": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateGraphQLSchema.ts",
"dev:generate-types": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateTypes.ts",
"dev:postgres": "cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DATABASE=postgres node ./test/dev.js",
"devsafe": "rimraf .next && pnpm dev",
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
"fix": "eslint \"packages/**/*.ts\" --fix",
"lint": "eslint \"packages/**/*.ts\"",
"lint-staged": "lint-staged",
"obliterate-playwright-cache": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"prepare": "husky install",
"reinstall": "pnpm clean:all && pnpm install",
"release:alpha": "tsx ./scripts/release.ts --bump prerelease --tag alpha",
@@ -123,6 +124,7 @@
"husky": "^8.0.3",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"json-schema-to-typescript": "11.0.3",
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
@@ -158,7 +160,7 @@
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
},
"engines": {
"node": ">=18.20.2",
"node": "^18.20.2 || >=20.9.0",
"pnpm": "^8.15.7"
},
"pnpm": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.47",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -5,7 +5,7 @@ import path from 'path'
import type { DbType, StorageAdapterType } from '../types.js'
import { warning } from '../utils/log.js'
import { configReplacements, dbReplacements, storageReplacements } from './replacements.js'
import { dbReplacements, storageReplacements } from './replacements.js'
/** Update payload config with necessary imports and adapters */
export async function configurePayloadConfig(args: {
@@ -15,8 +15,8 @@ export async function configurePayloadConfig(args: {
}
packageJsonName?: string
projectDirOrConfigPath: { payloadConfigPath: string } | { projectDir: string }
storageAdapter?: StorageAdapterType
sharp?: boolean
storageAdapter?: StorageAdapterType
}): Promise<void> {
if (!args.dbType) {
return
@@ -93,32 +93,32 @@ export async function configurePayloadConfig(args: {
const dbReplacement = dbReplacements[args.dbType]
configLines = replaceInConfigLines({
replacement: dbReplacement.configReplacement(args.envNames?.dbUri),
startMatch: `// database-adapter-config-start`,
endMatch: `// database-adapter-config-end`,
lines: configLines,
replacement: dbReplacement.configReplacement(args.envNames?.dbUri),
startMatch: `// database-adapter-config-start`,
})
configLines = replaceInConfigLines({
lines: configLines,
replacement: [dbReplacement.importReplacement],
startMatch: '// database-adapter-import',
lines: configLines,
})
// Storage Adapter Replacement
if (args.storageAdapter) {
const replacement = storageReplacements[args.storageAdapter]
configLines = replaceInConfigLines({
lines: configLines,
replacement: replacement.configReplacement,
startMatch: '// storage-adapter-placeholder',
lines: configLines,
})
if (replacement?.importReplacement) {
configLines = replaceInConfigLines({
lines: configLines,
replacement: [replacement.importReplacement],
startMatch: '// storage-adapter-import-placeholder',
lines: configLines,
})
}
}
@@ -126,14 +126,14 @@ export async function configurePayloadConfig(args: {
// Sharp Replacement (provided by default, only remove if explicitly set to false)
if (args.sharp === false) {
configLines = replaceInConfigLines({
lines: configLines,
replacement: [],
startMatch: 'sharp,',
lines: configLines,
})
configLines = replaceInConfigLines({
lines: configLines,
replacement: [],
startMatch: "import sharp from 'sharp'",
lines: configLines,
})
}
@@ -146,16 +146,16 @@ export async function configurePayloadConfig(args: {
}
function replaceInConfigLines({
replacement,
startMatch,
endMatch,
lines,
replacement,
startMatch,
}: {
replacement: string[]
startMatch: string
/** Optional endMatch to replace multiple lines */
endMatch?: string
lines: string[]
replacement: string[]
startMatch: string
}) {
if (!replacement) {
return lines

View File

@@ -66,9 +66,9 @@ const diskReplacement: StorageAdapterReplacement = {
}
export const storageReplacements: Record<StorageAdapterType, StorageAdapterReplacement> = {
localDisk: diskReplacement,
payloadCloud: payloadCloudReplacement,
vercelBlobStorage: vercelBlobStorageReplacement,
localDisk: diskReplacement,
}
/**

View File

@@ -4,8 +4,7 @@ import chalk from 'chalk'
import { Syntax, parseModule } from 'esprima-next'
import fs from 'fs'
import { warning } from '../utils/log.js'
import { log } from '../utils/log.js'
import { log , warning } from '../utils/log.js'
export const withPayloadStatement = {
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`,

View File

@@ -77,4 +77,4 @@ export type NextAppDetails = {
export type NextConfigType = 'cjs' | 'esm'
export type StorageAdapterType = 'payloadCloud' | 'vercelBlobStorage' | 'localDisk'
export type StorageAdapterType = 'localDisk' | 'payloadCloud' | 'vercelBlobStorage'

View File

@@ -3,8 +3,7 @@ import chalk from 'chalk'
import path from 'path'
import terminalLink from 'terminal-link'
import type { ProjectTemplate } from '../types.js'
import type { PackageManager } from '../types.js'
import type { PackageManager , ProjectTemplate } from '../types.js'
import { getValidTemplates } from '../lib/templates.js'

View File

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

View File

@@ -1,6 +1,5 @@
import type { CreateGlobalVersion } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { Document , PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'

View File

@@ -1,6 +1,5 @@
import type { CreateVersion } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { Document , PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'

View File

@@ -1,6 +1,5 @@
import type { DeleteOne } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { Document , PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'

View File

@@ -1,7 +1,6 @@
import type { MongooseQueryOptions } from 'mongoose'
import type { FindOne } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { Document , PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'

View File

@@ -1,13 +1,11 @@
import type { Payload } from 'payload'
import type { PathToQuery } from 'payload/database'
import type { Field } from 'payload/types'
import type { Operator } from 'payload/types'
import type { Field , Operator } from 'payload/types'
import ObjectIdImport from 'bson-objectid'
import mongoose from 'mongoose'
import { getLocalizedPaths } from 'payload/database'
import { fieldAffectsData } from 'payload/types'
import { validOperators } from 'payload/types'
import { fieldAffectsData , validOperators } from 'payload/types'
import type { MongooseAdapter } from '../index.js'

View File

@@ -2,8 +2,7 @@
/* eslint-disable no-await-in-loop */
import type { FilterQuery } from 'mongoose'
import type { Payload } from 'payload'
import type { Operator, Where } from 'payload/types'
import type { Field } from 'payload/types'
import type { Field, Operator , Where } from 'payload/types'
import deepmerge from 'deepmerge'
import { validOperators } from 'payload/types'

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.47",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {
@@ -42,7 +42,7 @@
"payload": "workspace:*"
},
"engines": {
"node": ">=18.20.2"
"node": "^18.20.2 || >=20.9.0"
},
"publishConfig": {
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.47",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {
@@ -40,7 +40,7 @@
"payload": "workspace:*"
},
"engines": {
"node": ">=18.20.2"
"node": "^18.20.2 || >=20.9.0"
},
"publishConfig": {
"exports": {

View File

@@ -10,6 +10,7 @@ const baseRules = {
'no-use-before-define': 'off',
'object-shorthand': 'warn',
'no-useless-escape': 'warn',
'import/no-duplicates': 'warn',
'perfectionist/sort-objects': [
'error',
{
@@ -123,7 +124,7 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: [], // Plugins are defined in the overrides to be more specific and only target the files they are meant for.
plugins: ['import'], // Plugins are defined in the overrides to be more specific and only target the files they are meant for.
overrides: [
{
files: ['**/*.ts'],

View File

@@ -21,6 +21,7 @@
"@typescript-eslint/parser": "7.3.1",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-jest-dom": "5.1.0",
"eslint-plugin-jsx-a11y": "6.8.0",

View File

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

View File

@@ -0,0 +1 @@
export { GraphQLJSON, GraphQLJSONObject } from '../packages/graphql-type-json/index.js'

View File

@@ -1 +1,2 @@
export { generateSchema } from '../bin/generateSchema.js'
export { buildObjectType } from '../schema/buildObjectType.js'

View File

@@ -1,5 +1,4 @@
import type { PayloadRequestWithData, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection, PayloadRequestWithData , Where } from 'payload/types'
import { countOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -1,6 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection , PayloadRequestWithData } from 'payload/types'
import type { MarkOptional } from 'ts-essentials'
import { createOperation } from 'payload/operations'

View File

@@ -1,6 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection , PayloadRequestWithData } from 'payload/types'
import { deleteByIDOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -1,6 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection , PayloadRequestWithData } from 'payload/types'
import { duplicateOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -1,6 +1,5 @@
import type { PaginatedDocs } from 'payload/database'
import type { PayloadRequestWithData, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection, PayloadRequestWithData , Where } from 'payload/types'
import { findOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -1,6 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection , PayloadRequestWithData } from 'payload/types'
import { findByIDOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -1,5 +1,4 @@
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection, TypeWithID } from 'payload/types'
import type { Collection , PayloadRequestWithData, TypeWithID } from 'payload/types'
import type { TypeWithVersion } from 'payload/versions'
import { findVersionByIDOperation } from 'payload/operations'

View File

@@ -1,6 +1,5 @@
import type { PaginatedDocs } from 'payload/database'
import type { PayloadRequestWithData, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection, PayloadRequestWithData , Where } from 'payload/types'
import { findVersionsOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -1,5 +1,4 @@
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection , PayloadRequestWithData } from 'payload/types'
import { restoreVersionOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -1,6 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import type { Collection , PayloadRequestWithData } from 'payload/types'
import { updateByIDOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'

View File

@@ -37,8 +37,7 @@ import {
GraphQLString,
} from 'graphql'
import { fieldAffectsData, optionIsObject, tabHasName } from 'payload/types'
import { toWords } from 'payload/utilities'
import { flattenTopLevelFields } from 'payload/utilities'
import { flattenTopLevelFields , toWords } from 'payload/utilities'
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
import combineParentName from '../utilities/combineParentName.js'

View File

@@ -41,6 +41,7 @@ import {
GraphQLUnionType,
} from 'graphql'
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
import { MissingEditorProp } from 'payload/errors'
import { tabHasName } from 'payload/types'
import { createDataloaderCacheKey, toWords } from 'payload/utilities'
@@ -80,7 +81,7 @@ type Args = {
parentName: string
}
function buildObjectType({
export function buildObjectType({
name,
baseFields = {},
config,
@@ -476,6 +477,10 @@ function buildObjectType({
async resolve(parent, args, context: Context) {
let depth = config.defaultDepth
if (typeof args.depth !== 'undefined') depth = args.depth
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
@@ -487,13 +492,13 @@ function buildObjectType({
// is run here again, with the provided depth.
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
if (editor?.populationPromises) {
if (editor?.graphQLPopulationPromises) {
const fieldPromises = []
const populationPromises = []
const populateDepth =
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
editor?.populationPromises({
editor?.graphQLPopulationPromises({
context,
depth: populateDepth,
draft: args.draft,
@@ -693,5 +698,3 @@ function buildObjectType({
return newlyCreatedBlockType
}
export default buildObjectType

View File

@@ -37,7 +37,7 @@ import restoreVersionResolver from '../resolvers/collections/restoreVersion.js'
import { updateResolver } from '../resolvers/collections/update.js'
import formatName from '../utilities/formatName.js'
import { buildMutationInputType, getCollectionIDType } from './buildMutationInputType.js'
import buildObjectType from './buildObjectType.js'
import { buildObjectType } from './buildObjectType.js'
import buildPaginatedListType from './buildPaginatedListType.js'
import { buildPolicyType } from './buildPoliciesType.js'
import buildWhereInputType from './buildWhereInputType.js'

View File

@@ -17,7 +17,7 @@ import restoreVersionResolver from '../resolvers/globals/restoreVersion.js'
import updateResolver from '../resolvers/globals/update.js'
import formatName from '../utilities/formatName.js'
import { buildMutationInputType } from './buildMutationInputType.js'
import buildObjectType from './buildObjectType.js'
import { buildObjectType } from './buildObjectType.js'
import buildPaginatedListType from './buildPaginatedListType.js'
import { buildPolicyType } from './buildPoliciesType.js'
import buildWhereInputType from './buildWhereInputType.js'

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.47",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -54,8 +54,8 @@
"path-to-regexp": "^6.2.1",
"qs": "6.11.2",
"react-diff-viewer-continued": "3.2.6",
"react-toastify": "10.0.5",
"sass": "1.77.4",
"sonner": "^1.5.0",
"ws": "^8.16.0"
},
"devDependencies": {
@@ -84,7 +84,7 @@
"payload": "workspace:*"
},
"engines": {
"node": ">=18.20.2"
"node": "^18.20.2 || >=20.9.0"
},
"publishConfig": {
"exports": {

View File

@@ -11,7 +11,6 @@ import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { parseCookies } from 'payload/auth'
import { createClientConfig } from 'payload/config'
import React from 'react'
import 'react-toastify/dist/ReactToastify.css'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'

View File

@@ -1,6 +1,5 @@
import httpStatus from 'http-status'
import { extractJWT } from 'payload/auth'
import { generatePayloadCookie } from 'payload/auth'
import { extractJWT , generatePayloadCookie } from 'payload/auth'
import { refreshOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'

View File

@@ -407,6 +407,11 @@ export const POST =
let res: Response
let collection: Collection
const overrideHttpMethod = request.headers.get('X-HTTP-Method-Override')
if (overrideHttpMethod === 'GET') {
return await GET(config)(request, { params: { slug } })
}
try {
req = await createPayloadRequest({
config,

View File

@@ -1,5 +1,5 @@
@import './styles.scss';
@import './toastify.scss';
@import './toasts.scss';
@import './colors.scss';
:root {

View File

@@ -1,58 +0,0 @@
@import 'vars';
.Toastify {
.Toastify__toast-container {
left: base(5);
transform: none;
right: base(5);
width: auto;
}
.Toastify__toast {
padding: base(0.5);
border-radius: $style-radius-m;
font-weight: 600;
}
.Toastify__close-button {
align-self: center;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.Toastify__toast--success {
color: var(--color-success-900);
background: var(--color-success-500);
.Toastify__progress-bar {
background-color: var(--color-success-900);
}
}
.Toastify__close-button--success {
color: var(--color-success-900);
}
.Toastify__toast--error {
background: var(--theme-error-500);
color: #fff;
.Toastify__progress-bar {
background-color: #fff;
}
}
.Toastify__close-button--light {
color: inherit;
}
@include mid-break {
.Toastify__toast-container {
left: $baseline;
right: $baseline;
}
}
}

View File

@@ -0,0 +1,111 @@
.payload-toast-container {
.payload-toast-close-button {
left: unset;
right: 0.5rem;
top: 1.55rem;
color: var(--theme-elevation-400);
background: unset;
border: none;
display: flex;
width: 1.25rem;
height: 1.25rem;
justify-content: center;
align-items: center;
&:hover {
background: none;
}
svg {
width: 2rem;
height: 2rem;
}
[dir='RTL'] & {
right: unset;
left: 0.5rem;
}
}
.payload-toast-item {
padding: 1rem 2.5rem 1rem 1rem;
color: var(--theme-text);
font-style: normal;
font-weight: 600;
display: flex;
gap: 1rem;
align-items: center;
width: 100%;
border-radius: 0.15rem;
border: 1px solid var(--theme-border-color);
background: var(--theme-input-bg);
box-shadow:
0px 10px 4px -8px rgba(0, 2, 4, 0.02),
0px 2px 3px 0px rgba(0, 2, 4, 0.05);
.toast-content {
transition: opacity 100ms cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
&[data-front='false'] {
.toast-content {
opacity: 0;
}
}
&[data-expanded='true'] {
.toast-content {
opacity: 1;
}
}
.toast-icon {
svg {
width: 2.4rem;
height: 2.4rem;
}
}
&.toast-warning {
border-color: var(--theme-warning-200);
background-color: var(--theme-warning-100);
}
&.toast-error {
border-color: var(--theme-error-300);
background-color: var(--theme-error-150);
}
&.toast-success {
border-color: var(--theme-success-200);
background-color: var(--theme-success-100);
}
&.toast-info {
border-color: var(--theme-elevation-250);
background-color: var(--theme-elevation-100);
}
[data-theme='light'] & {
&.toast-warning {
border-color: var(--theme-warning-550);
background-color: var(--theme-warning-100);
}
&.toast-error {
border-color: var(--theme-error-200);
background-color: var(--theme-error-50);
}
&.toast-success {
border-color: var(--theme-success-550);
background-color: var(--theme-success-50);
}
&.toast-info {
border-color: var(--theme-border-color);
background-color: var(--theme-elevation-50);
}
}
}
}

View File

@@ -1,8 +1,7 @@
import type { CustomPayloadRequestProperties, PayloadRequest, SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { executeAuthStrategies } from 'payload/auth'
import { parseCookies } from 'payload/auth'
import { executeAuthStrategies , parseCookies } from 'payload/auth'
import { getDataLoader } from 'payload/utilities'
import qs from 'qs'
import { URL } from 'url'
@@ -59,6 +58,17 @@ export const createPayloadRequest = async ({
fallbackLocale = locales.fallbackLocale
}
const overrideHttpMethod = request.headers.get('X-HTTP-Method-Override')
const queryToParse = overrideHttpMethod === 'GET' ? await request.text() : urlProperties.search
const query = queryToParse
? qs.parse(queryToParse, {
arrayLimit: 1000,
depth: 10,
ignoreQueryPrefix: true,
})
: {}
const customRequest: CustomPayloadRequestProperties = {
context: {},
fallbackLocale,
@@ -75,13 +85,7 @@ export const createPayloadRequest = async ({
payloadUploadSizes: {},
port: urlProperties.port,
protocol: urlProperties.protocol,
query: urlProperties.search
? qs.parse(urlProperties.search, {
arrayLimit: 1000,
depth: 10,
ignoreQueryPrefix: true,
})
: {},
query,
routeParams: params || {},
search: urlProperties.search,
searchParams: urlProperties.searchParams,

View File

@@ -36,6 +36,16 @@ 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
// Generate types
if (config.typescript.autoGenerate !== false) {
// We cannot run it directly here, as generate-types imports json-schema-to-typescript, which breaks on turbopack.
// see: https://github.com/vercel/next.js/issues/66723
void payload.bin({
args: ['generate:types'],
log: false,
})
}
await payload.db.init()
if (payload.db.connect) {
await payload.db.connect({ hotReload: true })

View File

@@ -1,4 +1,5 @@
import type { I18nClient } from '@payloadcms/translations'
import type { Locale } from 'payload/config'
import type { InitPageResult, PayloadRequestWithData, VisibleEntities } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
@@ -22,7 +23,6 @@ export const initPage = async ({
searchParams,
}: Args): Promise<InitPageResult> => {
const headers = getHeaders()
const localeParam = searchParams?.locale as string
const payload = await getPayloadHMR({ config: configPromise })
const {
@@ -34,10 +34,6 @@ export const initPage = async ({
} = payload.config
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const localeCode = localeParam || defaultLocale
const locale = localization && findLocaleFromCode(localization, localeCode)
const cookies = parseCookies(headers)
const language = getRequestLanguage({ config: payload.config, cookies, headers })
@@ -64,7 +60,6 @@ export const initPage = async ({
const req = await createLocalReq(
{
fallbackLocale: null,
locale: locale.code,
req: {
host: headers.get('host'),
i18n,
@@ -79,9 +74,53 @@ export const initPage = async ({
)
const { permissions, user } = await payload.auth({ headers, req })
req.user = user
const localeParam = searchParams?.locale as string
let locale: Locale
if (localization) {
const defaultLocaleCode = localization.defaultLocale ? localization.defaultLocale : 'en'
let localeCode: string = localeParam
if (!localeCode) {
try {
localeCode = await payload
.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
user,
where: {
and: [
{
'user.relationTo': {
equals: payload.config.admin.user,
},
},
{
'user.value': {
equals: user.id,
},
},
{
key: {
equals: 'locale',
},
},
],
},
})
?.then((res) => res.docs?.[0]?.value as string)
} catch (error) {} // eslint-disable-line no-empty
}
locale = findLocaleFromCode(localization, localeCode)
if (!locale) locale = findLocaleFromCode(localization, defaultLocaleCode)
req.locale = locale.code
}
const visibleEntities: VisibleEntities = {
collections: collections
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))

View File

@@ -2,8 +2,7 @@ import type { Metadata } from 'next'
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
import type { MetaConfig } from 'payload/config'
import { staticOGImage } from '@payloadcms/ui/assets'
import { payloadFaviconDark, payloadFaviconLight } from '@payloadcms/ui/assets'
import { payloadFaviconDark , payloadFaviconLight, staticOGImage } from '@payloadcms/ui/assets'
import QueryString from 'qs'
const defaultOpenGraph = {

View File

@@ -15,7 +15,7 @@ import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { useSearchParams } from 'next/navigation.js'
import qs from 'qs'
import * as React from 'react'
import { toast } from 'react-toastify'
import { toast } from 'sonner'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import { LocaleSelector } from './LocaleSelector/index.js'

View File

@@ -91,7 +91,7 @@ export const Account: React.FC<AdminViewProps> = async ({
initialParams={{
depth: 0,
'fallback-locale': 'null',
locale: locale.code,
locale: locale?.code,
uploadEdits: undefined,
}}
>

View File

@@ -26,7 +26,7 @@ export const getDocumentData = async (args: {
id,
collectionSlug: collectionConfig?.slug,
globalSlug: globalConfig?.slug,
locale: locale.code,
locale: locale?.code,
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
schemaPath: collectionConfig?.slug || globalConfig?.slug,
},

View File

@@ -10,6 +10,7 @@ import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth'
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
import { isEditing as getIsEditing } from '@payloadcms/ui/utilities/isEditing'
import { notFound, redirect } from 'next/navigation.js'
import QueryString from 'qs'
import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
@@ -85,10 +86,14 @@ export const Document: React.FC<AdminViewProps> = async ({
}
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}?locale=${locale.code}${
collectionConfig.versions?.drafts ? '&draft=true' : ''
}`
const apiQueryParams = QueryString.stringify(
{
draft: collectionConfig.versions?.drafts ? 'true' : undefined,
locale: locale?.code,
},
{ addQueryPrefix: true },
)
apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}${apiQueryParams}`
const editConfig = collectionConfig?.admin?.components?.views?.Edit
ViewOverride = typeof editConfig === 'function' ? editConfig : null
@@ -118,9 +123,14 @@ export const Document: React.FC<AdminViewProps> = async ({
action = `${serverURL}${apiRoute}/globals/${globalSlug}`
apiURL = `${serverURL}${apiRoute}/${globalSlug}?locale=${locale.code}${
globalConfig.versions?.drafts ? '&draft=true' : ''
}`
const apiQueryParams = QueryString.stringify(
{
draft: globalConfig.versions?.drafts ? 'true' : undefined,
locale: locale?.code,
},
{ addQueryPrefix: true },
)
apiURL = `${serverURL}${apiRoute}/${globalSlug}${apiQueryParams}`
const editConfig = globalConfig?.admin?.components?.views?.Edit
ViewOverride = typeof editConfig === 'function' ? editConfig : null
@@ -151,15 +161,17 @@ export const Document: React.FC<AdminViewProps> = async ({
hasSavePermission &&
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
const validateDraftData =
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
if (shouldAutosave && !id && collectionSlug) {
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
const doc = await payload.create({
collection: collectionSlug,
data: {},
depth: 0,
draft: true,
fallbackLocale: null,
locale: locale.code,
locale: locale?.code,
req,
user,
})
@@ -204,12 +216,15 @@ export const Document: React.FC<AdminViewProps> = async ({
/>
)}
<HydrateClientUser permissions={permissions} user={user} />
<EditDepthProvider depth={1} key={`${collectionSlug || globalSlug}-${locale.code}`}>
<EditDepthProvider
depth={1}
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
>
<FormQueryParamsProvider
initialParams={{
depth: 0,
'fallback-locale': 'null',
locale: locale.code,
locale: locale?.code,
uploadEdits: undefined,
}}
>

View File

@@ -10,7 +10,7 @@ import { useConfig } from '@payloadcms/ui/providers/Config'
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useCallback, useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { toast } from 'sonner'
import type { Props } from './types.js'
@@ -71,7 +71,7 @@ export const Auth: React.FC<Props> = (props) => {
})
if (response.status === 200) {
toast.success(t('authentication:successfullyUnlocked'), { autoClose: 3000 })
toast.success(t('authentication:successfullyUnlocked'))
} else {
toast.error(t('authentication:failedToUnlock'))
}

View File

@@ -9,7 +9,7 @@ import { useConfig } from '@payloadcms/ui/providers/Config'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { email } from 'payload/fields/validations'
import React, { Fragment, useState } from 'react'
import { toast } from 'react-toastify'
import { toast } from 'sonner'
export const ForgotPasswordForm: React.FC = () => {
const config = useConfig()

View File

@@ -97,7 +97,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
const sort =
query?.sort && typeof query.sort === 'string'
? query.sort
: listPreferences?.sort || undefined
: listPreferences?.sort || collectionConfig.defaultSort || undefined
const data = await payload.find({
collection: collectionSlug,

View File

@@ -10,16 +10,20 @@ export const DeviceContainer: React.FC<{
const { children } = props
const deviceFrameRef = React.useRef<HTMLDivElement>(null)
const outerFrameRef = React.useRef<HTMLDivElement>(null)
const { breakpoint, setMeasuredDeviceSize, size, zoom } = useLivePreviewContext()
const { breakpoint, setMeasuredDeviceSize, size: desiredSize, zoom } = useLivePreviewContext()
// Keep an accurate measurement of the actual device size as it is truly rendered
// This is helpful when `sizes` are non-number units like percentages, etc.
const { size: measuredDeviceSize } = useResize(deviceFrameRef)
const { size: measuredDeviceSize } = useResize(deviceFrameRef.current)
const { size: outerFrameSize } = useResize(outerFrameRef.current)
let deviceIsLargerThanFrame: boolean = false
// Sync the measured device size with the context so that other components can use it
// This happens from the bottom up so that as this component mounts and unmounts,
// Its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe
// its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe
useEffect(() => {
if (measuredDeviceSize) {
setMeasuredDeviceSize(measuredDeviceSize)
@@ -34,13 +38,34 @@ export const DeviceContainer: React.FC<{
if (
typeof zoom === 'number' &&
typeof size.width === 'number' &&
typeof size.height === 'number'
typeof desiredSize.width === 'number' &&
typeof desiredSize.height === 'number' &&
typeof measuredDeviceSize.width === 'number' &&
typeof measuredDeviceSize.height === 'number'
) {
const scaledWidth = size.width / zoom
const difference = scaledWidth - size.width
x = `${difference / 2}px`
margin = '0 auto'
const scaledDesiredWidth = desiredSize.width / zoom
const scaledDeviceWidth = measuredDeviceSize.width * zoom
const scaledDeviceDifferencePixels = scaledDesiredWidth - desiredSize.width
deviceIsLargerThanFrame = scaledDeviceWidth > outerFrameSize.width
if (deviceIsLargerThanFrame) {
if (zoom > 1) {
const differenceFromDeviceToFrame = measuredDeviceSize.width - outerFrameSize.width
if (differenceFromDeviceToFrame < 0) x = `${differenceFromDeviceToFrame / 2}px`
else x = '0'
} else {
x = '0'
}
} else {
if (zoom >= 1) {
x = `${scaledDeviceDifferencePixels / 2}px`
} else {
const differenceFromDeviceToFrame = outerFrameSize.width - scaledDeviceWidth
x = `${differenceFromDeviceToFrame / 2}px`
margin = '0'
}
}
}
}
@@ -48,21 +73,29 @@ export const DeviceContainer: React.FC<{
let height = zoom ? `${100 / zoom}%` : '100%'
if (breakpoint !== 'responsive') {
width = `${size?.width / (typeof zoom === 'number' ? zoom : 1)}px`
height = `${size?.height / (typeof zoom === 'number' ? zoom : 1)}px`
width = `${desiredSize?.width / (typeof zoom === 'number' ? zoom : 1)}px`
height = `${desiredSize?.height / (typeof zoom === 'number' ? zoom : 1)}px`
}
return (
<div
ref={deviceFrameRef}
ref={outerFrameRef}
style={{
height,
margin,
transform: `translate3d(${x}, 0, 0)`,
width,
height: '100%',
width: '100%',
}}
>
{children}
<div
ref={deviceFrameRef}
style={{
height,
margin,
transform: `translate3d(${x}, 0, 0)`,
width,
}}
>
{children}
</div>
</div>
)
}

View File

@@ -11,7 +11,7 @@ import { useConfig } from '@payloadcms/ui/providers/Config'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { useRouter } from 'next/navigation.js'
import React from 'react'
import { toast } from 'react-toastify'
import { toast } from 'sonner'
type Args = {
token: string
@@ -49,7 +49,7 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
history.push(`${admin}`)
} else {
history.push(`${admin}/login`)
toast.success(i18n.t('general:updatedSuccessfully'), { autoClose: 3000 })
toast.success(i18n.t('general:updatedSuccessfully'))
}
},
[fetchFullUser, history, admin, i18n],

View File

@@ -1,6 +1,5 @@
import type { CollectionPermission, GlobalPermission } from 'payload/auth'
import type { OptionObject } from 'payload/types'
import type { Document } from 'payload/types'
import type { Document , OptionObject } from 'payload/types'
export type CompareOption = {
label: string

View File

@@ -9,7 +9,7 @@ import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
import { requests } from '@payloadcms/ui/utilities/api'
import { useRouter } from 'next/navigation.js'
import React, { Fragment, useCallback, useState } from 'react'
import { toast } from 'react-toastify'
import { toast } from 'sonner'
import type { Props } from './types.js'

View File

@@ -28,6 +28,13 @@ export const withPayload = (nextConfig = {}) => {
'libsql',
],
},
turbo: {
...(nextConfig?.experimental?.turbo || {}),
resolveAlias: {
...(nextConfig?.experimental?.turbo?.resolveAlias || {}),
'payload-mock-package': 'payload-mock-package',
},
},
},
headers: async () => {
const headersFromConfig = 'headers' in nextConfig ? await nextConfig.headers() : []

View File

@@ -4,8 +4,6 @@ import { register } from 'node:module'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { bin } from './dist/bin/index.js'
// Allow disabling SWC for debugging
if (process.env.DISABLE_SWC !== 'true') {
const filename = fileURLToPath(import.meta.url)
@@ -15,4 +13,9 @@ if (process.env.DISABLE_SWC !== 'true') {
register('./dist/bin/loader/index.js', url)
}
bin()
const start = async () => {
const { bin } = await import('./dist/bin/index.js')
bin()
}
void start()

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.47",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -137,7 +137,7 @@
}
},
"engines": {
"node": "^18.20.2 || >=20.6.0"
"node": "^18.20.2 || >=20.9.0"
},
"publishConfig": {
"exports": {

View File

@@ -2,8 +2,10 @@ import type { GenericLanguages, I18n, I18nClient } from '@payloadcms/translation
import type { JSONSchema4 } from 'json-schema'
import type React from 'react'
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
import type { Field, FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { PayloadRequestWithData, RequestContext } from '../types/index.js'
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
@@ -15,6 +17,173 @@ export type RichTextFieldProps<
path?: string
}
export type AfterReadRichTextHookArgs<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = {
currentDepth?: number
depth?: number
draft?: boolean
fallbackLocale?: string
fieldPromises?: Promise<void>[]
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
findMany?: boolean
flattenLocales?: boolean
locale?: string
/** A string relating to which operation the field type is currently executing within. */
operation?: 'create' | 'delete' | 'read' | 'update'
overrideAccess?: boolean
populationPromises?: Promise<void>[]
showHiddenFields?: boolean
triggerAccessControl?: boolean
triggerHooks?: boolean
}
export type AfterChangeRichTextHookArgs<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = {
/** A string relating to which operation the field type is currently executing within. */
operation: 'create' | 'update'
/** The document before changes were applied. */
previousDoc?: TData
/** The sibling data of the document before changes being applied. */
previousSiblingDoc?: TData
/** The previous value of the field, before changes */
previousValue?: TValue
}
export type BeforeValidateRichTextHookArgs<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = {
/** A string relating to which operation the field type is currently executing within. */
operation: 'create' | 'update'
overrideAccess?: boolean
/** The sibling data of the document before changes being applied. */
previousSiblingDoc?: TData
/** The previous value of the field, before changes */
previousValue?: TValue
}
export type BeforeChangeRichTextHookArgs<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = {
/**
* The original data with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
*/
docWithLocales?: Record<string, unknown>
duplicate?: boolean
errors?: { field: string; message: string }[]
/** Only available in `beforeChange` field hooks */
mergeLocaleActions?: (() => Promise<void>)[]
/** A string relating to which operation the field type is currently executing within. */
operation?: 'create' | 'delete' | 'read' | 'update'
/** The sibling data of the document before changes being applied. */
previousSiblingDoc?: TData
/** The previous value of the field, before changes */
previousValue?: TValue
/**
* The original siblingData with locales (not modified by any hooks).
*/
siblingDocWithLocales?: Record<string, unknown>
skipValidation?: boolean
}
export type BaseRichTextHookArgs<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = {
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
collection: SanitizedCollectionConfig | null
context: RequestContext
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
data?: Partial<TData>
/** The field which the hook is running against. */
field: FieldAffectingData
/** The global which the field belongs to. If the field belongs to a collection, this will be null. */
global: SanitizedGlobalConfig | null
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
originalDoc?: TData
/**
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
*/
path: (number | string)[]
/** The Express request object. It is mocked for Local API operations. */
req: PayloadRequestWithData
/**
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
*/
schemaPath: string[]
/** The sibling data passed to a field that the hook is running against. */
siblingData: Partial<TSiblingData>
/** The value of the field. */
value?: TValue
}
export type AfterReadRichTextHook<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = (
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
AfterReadRichTextHookArgs<TData, TValue, TSiblingData>,
) => Promise<TValue> | TValue
export type AfterChangeRichTextHook<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = (
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
AfterChangeRichTextHookArgs<TData, TValue, TSiblingData>,
) => Promise<TValue> | TValue
export type BeforeChangeRichTextHook<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = (
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
BeforeChangeRichTextHookArgs<TData, TValue, TSiblingData>,
) => Promise<TValue> | TValue
export type BeforeValidateRichTextHook<
TData extends TypeWithID = any,
TValue = any,
TSiblingData = any,
> = (
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
BeforeValidateRichTextHookArgs<TData, TValue, TSiblingData>,
) => Promise<TValue> | TValue
export type RichTextHooks = {
afterChange?: AfterChangeRichTextHook[]
afterRead?: AfterReadRichTextHook[]
beforeChange?: BeforeChangeRichTextHook[]
beforeValidate?: BeforeValidateRichTextHook[]
}
type RichTextAdapterBase<
Value extends object = object,
AdapterProps = any,
@@ -32,7 +201,28 @@ type RichTextAdapterBase<
schemaMap: Map<string, Field[]>
schemaPath: string
}) => Map<string, Field[]>
hooks?: FieldBase['hooks']
/**
* Like an afterRead hook, but runs only for the GraphQL resolver. For populating data, this should be used, as afterRead hooks do not have a depth in graphQL.
*
* To populate stuff / resolve field hooks, mutate the incoming populationPromises or fieldPromises array. They will then be awaited in the correct order within payload itself.
* @param data
*/
graphQLPopulationPromises?: (data: {
context: RequestContext
currentDepth?: number
depth: number
draft: boolean
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
overrideAccess?: boolean
populationPromises: Promise<void>[]
req: PayloadRequestWithData
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
}) => void
hooks?: RichTextHooks
i18n?: Partial<GenericLanguages>
outputSchema?: ({
collectionIDFieldTypes,
@@ -50,27 +240,6 @@ type RichTextAdapterBase<
interfaceNameDefinitions: Map<string, JSONSchema4>
isRequired: boolean
}) => JSONSchema4
/**
* Like an afterRead hook, but runs for both afterRead AND in the GraphQL resolver. For populating data, this should be used.
*
* To populate stuff / resolve field hooks, mutate the incoming populationPromises or fieldPromises array. They will then be awaited in the correct order within payload itself.
* @param data
*/
populationPromises?: (data: {
context: RequestContext
currentDepth?: number
depth: number
draft: boolean
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
overrideAccess?: boolean
populationPromises: Promise<void>[]
req: PayloadRequestWithData
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
}) => void
validate: Validate<
Value,
Value,

View File

@@ -42,7 +42,7 @@ export type InitPageResult = {
docID?: string
globalConfig?: SanitizedGlobalConfig
languageOptions: LanguageOptions
locale: Locale
locale?: Locale
permissions: Permissions
req: PayloadRequestWithData
translations: ClientTranslationsObject

View File

@@ -129,8 +129,7 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
})
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
email.sendEmail({
await email.sendEmail({
from: `"${email.defaultFromName}" <${email.defaultFromAddress}>`,
html,
subject,

View File

@@ -1,5 +1,4 @@
import type { Payload, RequestContext } from '../../../index.js'
import type { GeneratedTypes } from '../../../index.js'
import type { GeneratedTypes, Payload , RequestContext } from '../../../index.js'
import type { PayloadRequestWithData } from '../../../types/index.js'
import type { Result } from '../login.js'

View File

@@ -1,5 +1,4 @@
import type { Payload, RequestContext } from '../../../index.js'
import type { GeneratedTypes } from '../../../index.js'
import type { GeneratedTypes, Payload , RequestContext } from '../../../index.js'
import type { PayloadRequestWithData } from '../../../types/index.js'
import type { Result } from '../resetPassword.js'

View File

@@ -1,5 +1,4 @@
import type { Payload, RequestContext } from '../../../index.js'
import type { GeneratedTypes } from '../../../index.js'
import type { GeneratedTypes, Payload , RequestContext } from '../../../index.js'
import type { PayloadRequestWithData } from '../../../types/index.js'
import { APIError } from '../../../errors/index.js'

View File

@@ -1,5 +1,4 @@
import type { Payload, RequestContext } from '../../../index.js'
import type { GeneratedTypes } from '../../../index.js'
import type { GeneratedTypes, Payload , RequestContext } from '../../../index.js'
import type { PayloadRequestWithData } from '../../../types/index.js'
import { APIError } from '../../../errors/index.js'

View File

@@ -2,8 +2,7 @@ import jwt from 'jsonwebtoken'
import url from 'url'
import type { BeforeOperationHook, Collection } from '../../collections/config/types.js'
import type { PayloadRequestWithData } from '../../types/index.js'
import type { Document } from '../../types/index.js'
import type { Document , PayloadRequestWithData } from '../../types/index.js'
import { buildAfterOperation } from '../../collections/operations/utils.js'
import { Forbidden } from '../../errors/index.js'

View File

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

View File

@@ -6,11 +6,16 @@ import type { SanitizedConfig } from '../config/types.js'
import { configToJSONSchema } from '../utilities/configToJSONSchema.js'
import Logger from '../utilities/logger.js'
export async function generateTypes(config: SanitizedConfig): Promise<void> {
export async function generateTypes(
config: SanitizedConfig,
options?: { log: boolean },
): Promise<void> {
const logger = Logger()
const outputFile = process.env.PAYLOAD_TS_OUTPUT_PATH || config.typescript.outputFile
logger.info('Compiling TS types for Collections and Globals...')
const shouldLog = options?.log ?? true
if (shouldLog) logger.info('Compiling TS types for Collections and Globals...')
const jsonSchema = configToJSONSchema(config, config.db.defaultIDType)
@@ -36,6 +41,18 @@ export async function generateTypes(config: SanitizedConfig): Promise<void> {
compiled += `\n\n${declare}`
}
}
// Diff the compiled types against the existing types file
try {
const existingTypes = fs.readFileSync(outputFile, 'utf-8')
if (compiled === existingTypes) {
return
}
} catch (_) {
// swallow err
}
fs.writeFileSync(outputFile, compiled)
logger.info(`Types written to ${outputFile}`)
if (shouldLog) logger.info(`Types written to ${outputFile}`)
}

View File

@@ -15,6 +15,7 @@ import { getBaseUploadFields } from '../../uploads/getBaseFields.js'
import { formatLabels } from '../../utilities/formatLabels.js'
import { isPlainObject } from '../../utilities/isPlainObject.js'
import baseVersionFields from '../../versions/baseFields.js'
import { versionDefaults } from '../../versions/defaults.js'
import { authDefaults, defaults } from './defaults.js'
export const sanitizeCollection = async (
@@ -84,15 +85,20 @@ export const sanitizeCollection = async (
if (sanitized.versions.drafts === true) {
sanitized.versions.drafts = {
autosave: false,
validate: false,
}
}
if (sanitized.versions.drafts.autosave === true) {
sanitized.versions.drafts.autosave = {
interval: 2000,
interval: versionDefaults.autosaveInterval,
}
}
if (sanitized.versions.drafts.validate === undefined) {
sanitized.versions.drafts.validate = false
}
sanitized.fields = mergeBaseFields(sanitized.fields, baseVersionFields)
}
}

View File

@@ -221,6 +221,7 @@ const collectionSchema = joi.object().keys({
interval: joi.number(),
}),
),
validate: joi.boolean(),
}),
joi.boolean(),
),

View File

@@ -165,14 +165,6 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
Promise.resolve(),
)
// /////////////////////////////////////
// Write files to local storage
// /////////////////////////////////////
// if (!collectionConfig.upload.disableLocalStorage) {
// await uploadFiles(payload, filesToUpload, req.t)
// }
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
@@ -203,7 +195,10 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
global: null,
operation: 'create',
req,
skipValidation: shouldSaveDraft,
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate,
})
// /////////////////////////////////////
@@ -268,8 +263,7 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
// /////////////////////////////////////
if (collectionConfig.auth && collectionConfig.auth.verify) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendVerificationEmail({
await sendVerificationEmail({
collection: { config: collectionConfig },
config: payload.config,
disableEmail: disableVerificationEmail,

View File

@@ -205,7 +205,10 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
global: null,
operation,
req,
skipValidation: shouldSaveDraft,
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate,
})
// set req.locale back to the original locale

View File

@@ -1,7 +1,6 @@
import type { MarkOptional } from 'ts-essentials'
import type { GeneratedTypes } from '../../../index.js'
import type { Payload } from '../../../index.js'
import type { GeneratedTypes , Payload } from '../../../index.js'
import type { Document, PayloadRequestWithData, RequestContext } from '../../../types/index.js'
import type { File } from '../../../uploads/types.js'

View File

@@ -1,7 +1,5 @@
import type { Payload } from '../../../index.js'
import type { GeneratedTypes } from '../../../index.js'
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
import type { Document, Where } from '../../../types/index.js'
import type { GeneratedTypes , Payload } from '../../../index.js'
import type { Document, PayloadRequestWithData , RequestContext, Where } from '../../../types/index.js'
import type { BulkOperationResult } from '../../config/types.js'
import { APIError } from '../../../errors/index.js'

View File

@@ -270,7 +270,10 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
operation: 'update',
req,
skipValidation:
Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate &&
data._status !== 'published',
})
// /////////////////////////////////////

View File

@@ -242,7 +242,11 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
global: null,
operation: 'update',
req,
skipValidation: Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate &&
data._status !== 'published',
})
// /////////////////////////////////////

View File

@@ -2,7 +2,7 @@ import type { Config } from './types.js'
export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
admin: {
avatar: 'default',
avatar: 'gravatar',
components: {},
custom: {},
dateFormat: 'MMMM do yyyy, h:mm a',
@@ -49,6 +49,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
serverURL: '',
telemetry: true,
typescript: {
autoGenerate: true,
outputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/payload-types.ts`,
},
upload: {},

View File

@@ -1,5 +1,5 @@
import { findUpSync, pathExistsSync } from 'find-up'
import fs from 'fs'
import { getTsconfig } from 'get-tsconfig'
import path from 'path'
/**
@@ -12,37 +12,30 @@ const getTSConfigPaths = (): {
outPath?: string
rootPath?: string
srcPath?: string
tsConfigPath?: string
} => {
const tsConfigPath = findUpSync('tsconfig.json')
if (!tsConfigPath) {
return {
rootPath: process.cwd(),
}
}
const tsConfigResult = getTsconfig()
const tsConfig = tsConfigResult.config
const tsConfigDir = path.dirname(tsConfigResult.path)
try {
// Read the file as a string and remove trailing commas
const rawTsConfig = fs
.readFileSync(tsConfigPath, 'utf-8')
.replace(/,\s*\]/g, ']')
.replace(/,\s*\}/g, '}')
const tsConfig = JSON.parse(rawTsConfig)
const rootPath = process.cwd()
const rootConfigDir = path.resolve(tsConfigDir, tsConfig.compilerOptions.baseUrl || '')
const srcPath = tsConfig.compilerOptions?.rootDir || path.resolve(process.cwd(), 'src')
const outPath = tsConfig.compilerOptions?.outDir || path.resolve(process.cwd(), 'dist')
const tsConfigDir = path.dirname(tsConfigPath)
let configPath = tsConfig.compilerOptions?.paths?.['@payload-config']?.[0]
let configPath = path.resolve(
rootConfigDir,
tsConfig.compilerOptions?.paths?.['@payload-config']?.[0],
)
if (configPath) {
configPath = path.resolve(tsConfigDir, configPath)
configPath = path.resolve(rootConfigDir, configPath)
}
return {
configPath,
outPath,
rootPath,
rootPath: rootConfigDir,
srcPath,
tsConfigPath: tsConfigResult.path,
}
} catch (error) {
console.error(`Error parsing tsconfig.json: ${error}`) // Do not throw the error, as we can still continue with the other config path finding methods
@@ -70,6 +63,11 @@ export const findConfig = (): string => {
const { configPath, outPath, rootPath, srcPath } = getTSConfigPaths()
// if configPath is absolute file, not folder, return it
if (path.extname(configPath) === '.js' || path.extname(configPath) === '.ts') {
return configPath
}
const searchPaths =
process.env.NODE_ENV === 'production'
? [configPath, outPath, srcPath, rootPath]

View File

@@ -69,7 +69,17 @@ export default joi.object({
meta: joi.object().keys({
defaultOGImageType: joi.string().valid('off', 'dynamic', 'static'),
description: joi.string(),
icons: joi.array().items(joi.object()),
icons: joi.array().items(
joi.object().keys({
type: joi.string(),
color: joi.string(),
fetchPriority: joi.string().valid('auto', 'high', 'low'),
media: joi.string(),
rel: joi.string(),
sizes: joi.string(),
url: joi.string(),
}),
),
openGraph: openGraphSchema,
titleSuffix: joi.string(),
}),
@@ -101,7 +111,7 @@ export default joi.object({
defaultMaxTextLength: joi.number(),
editor: joi
.object()
.required()
.optional()
.keys({
CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.optional(),
@@ -189,6 +199,7 @@ export default joi.object({
sharp: joi.any(),
telemetry: joi.boolean(),
typescript: joi.object({
autoGenerate: joi.boolean(),
declare: joi.alternatives().try(joi.boolean(), joi.object({ ignoreTSError: joi.boolean() })),
outputFile: joi.string(),
}),

View File

@@ -10,7 +10,7 @@ const ogImageObj = joi.object({
export const openGraphSchema = joi.object({
description: joi.string(),
images: joi.alternatives().try(joi.array().items(joi.string()), joi.array().items(ogImageObj)),
images: joi.alternatives().try(ogImageObj, joi.array().items(ogImageObj)),
title: joi.string(),
url: joi.string(),
siteName: joi.string(),
})

View File

@@ -120,7 +120,7 @@ export type MetaConfig = {
*
* For example browser tabs, phone home screens, and search engine results.
*/
icons?: IconConfig
icons?: IconConfig[]
/**
* Overrides the auto-generated <meta name="keywords"> of admin pages
* @example `"CMS, Payload, Custom"`
@@ -615,7 +615,7 @@ export type Config = {
*/
defaultMaxTextLength?: number
/** Default richtext editor to use for richText fields */
editor: RichTextAdapterProvider<any, any, any>
editor?: RichTextAdapterProvider<any, any, any>
/**
* Email Adapter
*
@@ -719,6 +719,12 @@ export type Config = {
telemetry?: boolean
/** Control how typescript interfaces are generated from your collections. */
typescript?: {
/**
* Automatically generate types during development
* @default true
*/
autoGenerate?: boolean
/** Disable declare block in generated types file */
declare?:
| {
@@ -732,6 +738,7 @@ export type Config = {
ignoreTSError?: boolean
}
| false
/** Filename to write the generated types to */
outputFile?: string
}
@@ -747,7 +754,7 @@ export type SanitizedConfig = Omit<
> & {
collections: SanitizedCollectionConfig[]
/** Default richtext editor to use for richText fields */
editor: RichTextAdapter<any, any, any>
editor?: RichTextAdapter<any, any, any>
endpoints: Endpoint[]
globals: SanitizedGlobalConfig[]
i18n: Required<I18nOptions>

View File

@@ -12,6 +12,7 @@ export { InvalidFieldName } from './InvalidFieldName.js'
export { InvalidFieldRelationship } from './InvalidFieldRelationship.js'
export { LockedAuth } from './LockedAuth.js'
export { MissingCollectionLabel } from './MissingCollectionLabel.js'
export { MissingEditorProp } from './MissingEditorProp.js'
export { MissingFieldInputOptions } from './MissingFieldInputOptions.js'
export { MissingFieldType } from './MissingFieldType.js'
export { MissingFile } from './MissingFile.js'

View File

@@ -2,8 +2,10 @@ export {
APIError,
AuthenticationError,
DuplicateCollection,
DuplicateFieldName,
DuplicateGlobal,
ErrorDeletingFile,
FileRetrievalError,
FileUploadError,
Forbidden,
InvalidConfiguration,
@@ -11,6 +13,7 @@ export {
InvalidFieldRelationship,
LockedAuth,
MissingCollectionLabel,
MissingEditorProp,
MissingFieldInputOptions,
MissingFieldType,
MissingFile,

View File

@@ -1,5 +1,6 @@
export { buildVersionCollectionFields } from '../versions/buildCollectionFields.js'
export { buildVersionGlobalFields } from '../versions/buildGlobalFields.js'
export { versionDefaults } from '../versions/defaults.js'
export { deleteCollectionVersions } from '../versions/deleteCollectionVersions.js'
export { enforceMaxVersions } from '../versions/enforceMaxVersions.js'
export { getLatestCollectionVersion } from '../versions/getLatestCollectionVersion.js'

View File

@@ -158,7 +158,7 @@ export const sanitizeFields = async ({
// config.editor should be sanitized at this point
field.editor = _config.editor
} else {
throw new MissingEditorProp(field)
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
}
@@ -172,25 +172,6 @@ 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 = {}
const mergeHooks = (hookName: keyof typeof field.editor.hooks) => {
if (typeof field.editor === 'function') return
if (field.editor?.hooks?.[hookName]?.length) {
field.hooks[hookName] = field.hooks[hookName]
? field.hooks[hookName].concat(field.editor.hooks[hookName])
: [...field.editor.hooks[hookName]]
}
}
mergeHooks('afterRead')
mergeHooks('afterChange')
mergeHooks('beforeChange')
mergeHooks('beforeValidate')
mergeHooks('beforeDuplicate')
}
if (richTextSanitizationPromises) {
richTextSanitizationPromises.push(sanitizeRichText)

View File

@@ -499,8 +499,8 @@ export const richText = baseField.keys({
CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.optional(),
afterReadPromise: joi.func().optional(),
graphQLPopulationPromises: joi.func().optional(),
outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})
.unknown(),

View File

@@ -41,16 +41,28 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
originalDoc?: TData
overrideAccess?: boolean
/**
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
*/
path: (number | string)[]
/** The document before changes were applied, only in `afterChange` hooks. */
previousDoc?: TData
/** The sibling data of the document before changes being applied, only in `beforeChange` and `afterChange` hook. */
/** The sibling data of the document before changes being applied, only in `beforeChange`, `beforeValidate`, `beforeDuplicate` and `afterChange` field hooks. */
previousSiblingDoc?: TData
/** The previous value of the field, before changes, only in `beforeChange`, `afterChange` and `beforeValidate` hooks. */
/** The previous value of the field, before changes, only in `beforeChange`, `afterChange`, `beforeDuplicate` and `beforeValidate` field hooks. */
previousValue?: TValue
/** The Express request object. It is mocked for Local API operations. */
req: PayloadRequestWithData
/**
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
*/
schemaPath: string[]
/** The sibling data passed to a field that the hook is running against. */
siblingData: Partial<TSiblingData>
/**
* The original siblingData with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
*/
siblingDocWithLocales?: Record<string, unknown>
/** The value of the field. */
value?: TValue
}

View File

@@ -0,0 +1,39 @@
import type { Field, TabAsField } from './config/types.js'
import { tabHasName } from './config/types.js'
export function getFieldPaths({
field,
parentPath,
parentSchemaPath,
}: {
field: Field | TabAsField
parentPath: (number | string)[]
parentSchemaPath: string[]
}): {
path: (number | string)[]
schemaPath: string[]
} {
if (field.type === 'tabs' || field.type === 'row' || field.type === 'collapsible') {
return {
path: parentPath,
schemaPath: parentSchemaPath,
}
} else if (field.type === 'tab') {
if (tabHasName(field)) {
return {
path: [...parentPath, field.name],
schemaPath: [...parentSchemaPath, field.name],
}
} else {
return {
path: parentPath,
schemaPath: parentSchemaPath,
}
}
}
const path = parentPath?.length ? [...parentPath, field.name] : [field.name]
const schemaPath = parentSchemaPath?.length ? [...parentSchemaPath, field.name] : [field.name]
return { path, schemaPath }
}

View File

@@ -8,7 +8,13 @@ import { traverseFields } from './traverseFields.js'
type Args<T> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
/**
* The data before hooks
*/
data: Record<string, unknown> | T
/**
* The data after hooks
*/
doc: Record<string, unknown> | T
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
@@ -24,7 +30,6 @@ export const afterChange = async <T extends Record<string, unknown>>({
collection,
context,
data,
doc: incomingDoc,
global,
operation,
@@ -41,9 +46,11 @@ export const afterChange = async <T extends Record<string, unknown>>({
fields: collection?.fields || global?.fields,
global,
operation,
path: [],
previousDoc,
previousSiblingDoc: previousDoc,
req,
schemaPath: [],
siblingData: data,
siblingDoc: doc,
})

View File

@@ -1,10 +1,13 @@
/* eslint-disable no-param-reassign */
import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import { getFieldPaths } from '../../getFieldPaths.js'
import { traverseFields } from './traverseFields.js'
type Args = {
@@ -15,6 +18,14 @@ type Args = {
field: Field | TabAsField
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
/**
* The parent's path
*/
parentPath: (number | string)[]
/**
* The parent's schemaPath (path without indexes).
*/
parentSchemaPath: string[]
previousDoc: Record<string, unknown>
previousSiblingDoc: Record<string, unknown>
req: PayloadRequestWithData
@@ -33,12 +44,20 @@ export const promise = async ({
field,
global,
operation,
parentPath,
parentSchemaPath,
previousDoc,
previousSiblingDoc,
req,
siblingData,
siblingDoc,
}: Args): Promise<void> => {
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
field,
parentPath,
parentSchemaPath,
})
if (fieldAffectsData(field)) {
// Execute hooks
if (field.hooks?.afterChange) {
@@ -53,12 +72,14 @@ export const promise = async ({
global,
operation,
originalDoc: doc,
path: fieldPath,
previousDoc,
previousSiblingDoc,
previousValue: previousDoc[field.name],
req,
schemaPath: fieldSchemaPath,
siblingData,
value: siblingData[field.name],
value: siblingDoc[field.name],
})
if (hookedValue !== undefined) {
@@ -79,9 +100,11 @@ export const promise = async ({
fields: field.fields,
global,
operation,
path: fieldPath,
previousDoc,
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
req,
schemaPath: fieldSchemaPath,
siblingData: (siblingData?.[field.name] as Record<string, unknown>) || {},
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
})
@@ -104,9 +127,11 @@ export const promise = async ({
fields: field.fields,
global,
operation,
path: [...fieldPath, i],
previousDoc,
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData?.[field.name]?.[i] || {},
siblingDoc: { ...row } || {},
}),
@@ -135,10 +160,12 @@ export const promise = async ({
fields: block.fields,
global,
operation,
path: [...fieldPath, i],
previousDoc,
previousSiblingDoc:
previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData?.[field.name]?.[i] || {},
siblingDoc: { ...row } || {},
}),
@@ -161,9 +188,11 @@ export const promise = async ({
fields: field.fields,
global,
operation,
path: fieldPath,
previousDoc,
previousSiblingDoc: { ...previousSiblingDoc },
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData || {},
siblingDoc: { ...siblingDoc },
})
@@ -190,9 +219,11 @@ export const promise = async ({
fields: field.fields,
global,
operation,
path: fieldPath,
previousDoc,
previousSiblingDoc: tabPreviousSiblingDoc,
req,
schemaPath: fieldSchemaPath,
siblingData: tabSiblingData,
siblingDoc: tabSiblingDoc,
})
@@ -209,15 +240,57 @@ export const promise = async ({
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
global,
operation,
path: fieldPath,
previousDoc,
previousSiblingDoc: { ...previousSiblingDoc },
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData || {},
siblingDoc: { ...siblingDoc },
})
break
}
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.afterChange?.length) {
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
path: fieldPath,
previousDoc,
previousSiblingDoc,
previousValue: previousDoc[field.name],
req,
schemaPath: fieldSchemaPath,
siblingData,
value: siblingDoc[field.name],
})
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue
}
}, Promise.resolve())
}
break
}
default: {
break
}

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