Compare commits

...

198 Commits

Author SHA1 Message Date
Dan Ribbens
d2c9ba5074 fix: immutable error from headers 2025-03-12 13:18:04 -04:00
Patrik
9d6583d9de fix: incorrect height rounding when resizing images with sharp (#11634)
This PR fixes an issue where the Sharp `.resize()` function would round
down an auto-scaled dimension when `fastShrinkOnLoad` was enabled
(enabled by default).

This caused slight discrepancies in height calculations in certain edge
cases.

Be default (`fastShrinkOnLoad: true`), Sharp:
- Uses the built-in shrink-on-load feature for JPEG and WebP
- It is an optimization that prioritizes speed over precision when
resizing images

By setting `fastShrinkOnLoad: false`, we force Sharp to:
- Perform a more accurate resize operation instead of relying on quick
pre-shrink methods.

### Before / Context:

- Upload an image with original dimensions of 1500 × 735
- Define an `imageSize` of the following:
```
{
  name: 'thumbnail',
  width: 300,
},
```

#### Calculation:

`originalAspectRatio = 1500 / 735 ≈ 2.04081632653`

`resizeHeight = 300 / 2.04081632653`
`resizeHeight = 147`

However, Sharp's `.resize()` calculation would output:

`resizeHeight = 146`

This lead to an error of:

```
[17:05:13] ERROR: extract_area: bad extract area
    err: {
      "type": "Error",
      "message": "extract_area: bad extract area",
      "stack":
          Error: extract_area: bad extract area
    }
```

### After:

Sharp's `.resize()` calculation now correctly outputs:

`resizeHeight = 147`
2025-03-12 09:48:05 -04:00
Marcus Forsberg
7be02194d6 fix(translations): improve Swedish translations (#11654)
### What?
Improves Swedish translations throughout.

- There were several places where the automatic translations didn't make
sense, particularily around localization where "locale" was incorrectly
referred to as "Lokal" instead of "Språk". "Crop" being translated to
"Skörd" was another hilarious one ("Skörd" means crop as in harvest 😊).
- Most success messages were overly formal in Swedish with
"framgångsrikt" being used in an awkward fashion. I've shortened them to
be more to the point.
- Some shorter strings had incorrect capitalization, such as "Nytt
Lösenord". Swedish doesn't use that kind of capitalization, so "Nytt
lösenord" is correct.
- Replaced "Manöverpanel" as the word for "Dashboard" with "Översikt"
which is less awkward.
- Normalized loading toasts throughout so they all use dots at the end
to signify an ongoing action such as "Laddar..".
- Several other small improvements to make things more natural.
2025-03-12 11:57:55 +00:00
Jesper We
1da50f5684 chore(translations): polish Swedish (#11353) 2025-03-11 22:51:15 -04:00
Jacob Fletcher
f2abc80a00 test: deflakes blocks e2e (#11640)
The blocks e2e tests were flaky due to how we conditionally render
fields as they enter the viewport. This prevented Playwright from every
reaching the target element when running
`locator.scrollIntoViewIfNeeded()`. This is especially flaky on pages
with many fields because the page size would continually grow as it was
scrolled.

To fix this there are new `scrollEntirePage` and `waitForPageStability`
helpers. Together, these will ensure that all fields are rendered and
fully loaded before we start testing. An early attempt at this was made
via `page.mouse.wheel(0, 1750)`, but this is an arbitrary pixel value
that is error prone and is not future proof.

These tests were also flaky by an attempt to trigger a form state action
before it was ready to receive events. The fix here is to disable any
buttons while the form is initializing and let Playwright wait for an
interactive state.
2025-03-11 22:49:06 -04:00
Alessio Gravili
88eeeaa8dd fix: incorrect types for field Label, Description and Error server components (#11642)
Our previous types for Label, Description and Error server components were incorrectly typed. We were using the `ServerProps` type, which was wrong.

In our renderFields function, you can see that `ServerComponentProps` are passed as server props, not `ServerProps`: https://github.com/payloadcms/payload/blob/fix/incorrect-component-types/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx

Additionally, we no longer have to wrap that type in `Partial<>`, as all server props in that type are required.
2025-03-11 20:48:11 -06:00
Alessio Gravili
d14bc44c63 docs: fix invalid ```txt language (#11638)
Fixes error when importing docs to website. `text` is a valid language,
`txt` is not.
2025-03-11 15:25:31 -06:00
Alessio Gravili
9c53a62503 chore(deps): bump next.js from 15.2.1 to 15.2.2 in monorepo (#11636)
This bumps next.js to 15.2.2 in our monorepo, guaranteeing compatibility
2025-03-11 17:21:23 -04:00
Elliot DeNolf
bc79608db4 chore(release): eslint/3.28.0 2025-03-11 17:19:36 -04:00
Elliot DeNolf
d959d843a2 chore(release): v3.28.0 [skip ci] 2025-03-11 17:10:15 -04:00
Germán Jabloñski
eb09ce9a3e feat(richtext-lexical): allow disabling indentation for specific nodes (#11631)
allow disabling indentation for specific nodes via IndentFeature

Usage: 

```ts
editor: lexicalEditor({
  features: ({ defaultFeatures }) => [
    ...defaultFeatures,
    IndentFeature({
      // the array must contain the "type" property of registered indentable nodes
      disabledNodes: ['paragraph', 'listitem'],
    }),
  ],
}),
```

The nodes "paragraph", "heading", "listitem", "quote" remain indentable
by default, even without `IndentFeature` registered.

In a future PR we will probably add the option to disable TabNode.
2025-03-11 17:27:25 -03:00
Alessio Gravili
f2da72b4d0 chore(deps): bump all eslint packages (#11629)
This bumps all eslint packages, ensuring compatibility with TypeScript 5.7.3. Previously, the following would be thrown:

```bash
WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.

You may find that it works just fine, or you may not.

SUPPORTED TYPESCRIPT VERSIONS: >=4.7.4 <5.7.0

YOUR TYPESCRIPT VERSION: 5.7.3

Please only submit bug reports when using the officially supported version
```

This [might have caused errors during linting](https://payloadcms.slack.com/archives/C04H7CQ615K/p1741707183505329?thread_ts=1741707036.030089&cid=C04H7CQ615K).

`payload` lint before: ✖ 380 problems (9 errors, 371 warnings)
`payload` lint after: ✖ 381 problems (9 errors, 372 warnings)

`ui` lint before: ✖ 154 problems (12 errors, 142 warnings)
`ui` lint after: ✖ 267 problems (12 errors, 255 warnings)

The additional warnings in `ui` come from the new  `@eslint-react/no-use-context` and  `@eslint-react/no-context-provider` rules which are good to have in React 19.
2025-03-11 18:34:50 +00:00
Jacob Fletcher
5285518562 feat: defaults to noindex nofollow (#11623)
We now have the ability to define all page metadata for the admin panel
via the Payload Config as a result of #11593. This means we can now set
sensible defaults for additional properties, e.g. `noindex` and
`nofollow` on the `robots` property. Setting this will prevent these
pages from being indexed and appearing in search results.

Note that setting this property prevents _indexing_ these pages, but
does not prevent them from being _crawled_. To prevent crawling as well,
you must add a standalone `robots.txt` file to your root directory.
2025-03-11 13:29:49 -04:00
Alessio Gravili
243cdb1901 refactor: more reliable import map generation, supporting turbopack and tsconfig basePath (#11618)
This simplifies and cleans up import map generation and adds support for turbopack, as well as the tsconfig `compilerOptions.basePath` property.

Previously, relative import paths looked like this:

```ts
import { TestComponent as ___ } from 'test/admin/components/TestComponent.js'
```

Paths like these will be resolved based on the `compilerOptions.baseUrl` path of your tsconfig.

This had 2 problems:

### baseUrl support

 If your tsconfig baseUrl was not `"."`, this did not work, as the import map generator does not respect it
 
 ### Turbopack support
 
If Turbopack was used, certain import paths were not able to be resolved.

For example, if your component is outside the `baseDir`, the generated path looked like this:

```ts
import { TestComponent as ___ } from '/../test/admin/components/TestComponent.js'
```

This works fine in webpack, but breaks in turbopack.

## Solution

This PR ensures all import paths are relative, making them more predictable and reliable.

The same component will now generate the following import path which works in Turbopack and if a different `compilerOptions.basePath` property is set:

```ts
import { TestComponent as ___ } from '../../../test/admin/components/TestComponent.js'
```

It also adds unit tests
2025-03-11 09:56:41 -06:00
Alessio Gravili
c7bb694249 perf: 50% faster compilation speed by skipping bundling of server-only packages during dev (#11594)
This PR skips bundling server-only payload packages during development, which results in 50% faster compilation speeds using turbo.

Test results using our blank template (both /api and /admin):

Webpack before: 11.5
Webpack now: 7.1s
=> 38% faster compilation speed

Turbopack before: 4.1s
Turbopack after: 2.1s
=> 50% faster compilation speed
2025-03-11 09:45:13 -06:00
Patrik
8f3d1bd871 fix: ensure only authenticated users can access the payload-locked-documents collection (#11624) 2025-03-11 10:57:12 -04:00
Rokas Puzonas
85f88a0194 fix(translations): update translation placeholders to not be translated for lithuanian (#11622)
### What?
The Lithuanian i18n translations have the placeholders (i.e.
`{{label}}`) also translated. For example `{{label}}` to `{{žymė}}`

My guess is that this was caused by the `pnpm translateNewKeys` script
which feeds all of the strings to OpenAI. In the system message in
[translateText.ts#L15](https://github.com/payloadcms/payload/blob/main/packages/translations/scripts/translateNewKeys/translateText.ts#L15)
there is nothing mentioning that it should not translate placeholders.
But I guess the AI was clever enough most of the time and not translated
them, leaving them as is. Because in the Lithuanian translation most
placeholders were correctly left as is, but a couple of them weren't.

I would have updated the system message, but I struggled to setup my
environment so that `pnpm translateNewKeys` would work (probably because
I'm on windows, idk). So I'm leaving the system message as is because I
can't test my changes, someone else should update it in another PR.

### Why?
Lithuanian messages weren't translated correctly.

### How?
Manually went through all of the used placeholders in in `lt.ts` and
updated the ones which were translated. Double checked using `en.ts`
file to see what was the original placeholder name.
2025-03-11 14:45:18 +00:00
Germán Jabloñski
38f61e91b8 docs: fix documentation about custom i18n types (#11386)
Fixes #9858

# The problems

There were several issues with custom i18n typing in the documentation
that were not detected because they did not occur in non-strict ts mode.

1. `Config['i18n']['translations']` didn't work, because i18n is an
optional property. As described in
[#9858](https://github.com/payloadcms/payload/issues/9858#issuecomment-2555814771),
some users were getting around this with
`NonNullable<Config['i18n']>['translations']`
2. [The trick being attempted in
`i18n`](36e152d69d/packages/payload/src/config/types.ts (L1034))
to customize and extend the `DefaultTranslationObject` does not work.
`i18n?: I18nOptions<{} | DefaultTranslationsObject> // loosen the type
here to allow for custom translations`.

If you want to verify this, you can use the following code example:
```ts
import type { Config } from 'payload'

const translation: NonNullable<Config['i18n']>['translations'] = {
  en: {
    authentication: {
      aaaaa: 'aaaaa', // I chose `authentication.aaaa` to appear first in intellisense
    }
  },
}

translation.en?.authentication // Property 'authentication' does not 
// exist on type '{} | { authentication: { account: string...
// so this option doesn't let you access the keys because of the join with `{}`, 
// and even if it did, it's not adding `aaaa` as a key.
```
3. In places where the `t` function is exposed in a callback, you cannot
do what the documentation says:
`{ t }: { t: TFunction<CustomTranslationsKeys | DefaultTranslationKeys>
}`
The reason for this is that the callback is exposed as a `LabelFunction`
type but without type arguments, and as a default it uses
`DefaultTranslationKeys`, which does not allow additional keys.

If you want to verify this, you can use the following code example:
```ts
// Make sure to test this with ts in strict mode
const _labelFn: LabelFunction = ({ t }: { t: TFunction<'extraKey' | DefaultTranslationKeys> }) => ""
// Type '"extraKey"' is not assignable to type
// '"authentication:account" | ... 441 more ... | "version:versionCount"'.
```

# The solution

Point 1 is a documentation issue. We could use `NonNullable`, or expose
the `I18nOptions` type, or simply not define the custom translation type
(which makes sense because if you put it in the config, ts will warn you
anyway).

Points 2 and 3 should ideally be corrected at the type level, but it
would imply a breaking change.

For now, I have corrected them at the documentation level, using an
alternative for point 2 and a type cast for point 3.

Maybe in payload v4 we should revisit this.
2025-03-11 09:14:44 -03:00
Jacob Fletcher
ac1e3cf69e feat(ui): form state queues (#11579)
Implements a form state task queue. This will prevent onChange handlers
within the form component from processing unnecessarily often, sometimes
long after the user has stopped making changes. This leads to a
potentially huge number of network requests if those changes were made
slower than the debounce rate. This is especially noticeable on slow
networks.

Does so through a new `useQueue` hook. This hook maintains a stack of
events that need processing but only processes the final event to
arrive. Every time a new event is pushed to the stack, the currently
running process is aborted (if any), and that event becomes the next in
the queue. This results in a shocking reduction in the time it takes
between final change to form state and the final network response, from
~1.5 minutes to ~3 seconds (depending on the scenario, see below).

This likely fixes a number of existing open issues. I will link those
issues here once they are identified and verifiably fixed.

Before:

I'm typing slowly here to ensure my changes aren't debounce by the form.
There are a total of 60 characters typed, triggering 58 network requests
and taking around 1.5 minutes to complete after the final change was
made.


https://github.com/user-attachments/assets/49ba0790-a8f8-4390-8421-87453ff8b650

After:

Here there are a total of 69 characters typed, triggering 11 network
requests and taking only about 3 seconds to complete after the final
change was made.


https://github.com/user-attachments/assets/447f8303-0957-41bd-bb2d-9e1151ed9ec3
2025-03-10 21:25:14 -04:00
Jacob Fletcher
397c1f1ae7 feat(next): fully expose Next.js metadata (#11593)
Payload now fully exposes Next.js' metadata options. You can now use the
`admin.meta` config to set any properties that Next.js supports and
Payload will inject them into its `generateMetadata` function call. The
`MetaConfig` provided by Payload now directly extends the `Metadata`
type from Next.js.

Although `admin.meta` has always been available, it only supported a
subset of options, such as `title`, `openGraph`, etc., but was lacking
properties like `robots`, etc.
2025-03-10 21:24:55 -04:00
Germán Jabloñski
c8f01e31a1 chore(db-postgres): enable TypeScript strict (#11560) 2025-03-10 18:12:20 -03:00
Jessica Chowdhury
9ac7a3ed49 fix(ui): adds fallback locale when defaultLocale is unavailable (#11614) 2025-03-10 15:20:58 -04:00
Jessica Chowdhury
051c1fe015 chore(ui): code/json field full height should include any padding added (#11607) 2025-03-10 15:17:58 -04:00
Dan Ribbens
6d0924ef37 fix: upload imageSizes forces original file uploads to be compressed (#11612)
### What?

When the upload config contains imageSizes, we are forcing the image to
be resized using sharp. This leads to lossy compression even when the
formatOptions and no cropping or focal point selection was made. This
change makes it possible to upload the original image, skipping
compression while still using the imageSizes feature.

### Why?

It should be possible to upload files without compression.

### How?

Changes the conditions to remove imageSizes to determine if sharp image
processing should be applied to the original image or not.
2025-03-10 14:32:30 -04:00
Jarrod Flesch
fc5876a602 fix(ui): stale list thumbnails when navigating (#11609)
### What? Stale list view images
Thumbnail images are stale on slow connections.

### Why?
The variable `fileExists` is not reset when the `fileSrc` prop changes.

#### Before

https://github.com/user-attachments/assets/57a2352a-8312-4070-ba16-8c4f4d4e58e2

#### After

https://github.com/user-attachments/assets/ea44b460-823d-412a-bed0-425378480bb5
2025-03-10 14:14:00 -04:00
Paul
72efc843cc templates: fix issue with populateAuthors hook breaking live-preview on website template (#11608)
Fixes https://github.com/payloadcms/payload/issues/11468

The populateAuthors hook could break live preview if it returned a
notFound error as we didn't catch these properly
2025-03-10 17:42:30 +00:00
Patrik
3ede7abe00 feat: threads path through field validate function (#11591)
This PR updates the field `validate` function property to include a new
`path` argument.

The `path` arg provides the schema path of the field, including array
indices where applicable.

#### Changes:

- Added `path: (number | string)[]` in the ValidateOptions type.
2025-03-10 11:41:23 -04:00
Sasha
5d65cb002b fix(plugin-import-export): plugin breaks i18n configuration (#11590)
Fixes https://github.com/payloadcms/payload/issues/11582
2025-03-10 11:31:06 -04:00
Md. Tajmirul Islam Akhand
814ced463b templates: allow displaying dynamic error message on forms created via Form Builder plugin (#11275)
Close #11274

### Why this PR?
I've created a custom phone number input block for my form builder,
including validation. However, the component on the frontend only
displays the generic message "This field is required," even when
formState.errors contains specific error messages. This is not the
expected behavior. I need the component to display the error messages
from formState.errors.

### Description
This pull request includes changes to improve error handling in various
form components by passing the `name` prop to the `Error` component and
updating the `Error` component to display specific error messages.

#### Error handling improvements:

*
[`templates/website/src/blocks/Form/Error/index.tsx`](diffhunk://#diff-a97a4b2b87ff1a02431d11ab00f4e0ead5d11819f45dac120b9502ace520196fR1-R14):
Updated the `Error` component to accept a `name` prop and use
`useFormContext` to display specific error messages.

#### Form component updates:

*
[`templates/website/src/blocks/Form/Checkbox/index.tsx`](diffhunk://#diff-4f0ad9596965f1e3b2f6356943d1d34009a742502bc8ab8d438ce98593fdef4aL42-R42):
Modified to pass the `name` prop to the `Error` component.
*
[`templates/website/src/blocks/Form/Country/index.tsx`](diffhunk://#diff-3abd97c2bfe7ce2a1809e6eaac68e6c02078514308f964b1792f7a1af2df92a7L62-R62):
Modified to pass the `name` prop to the `Error` component.
*
[`templates/website/src/blocks/Form/Email/index.tsx`](diffhunk://#diff-f1be3cf1e7c1fa9b543ed8f56a3655e601fdb399d31ede1d099a37004a1861bfL35-R35):
Modified to pass the `name` prop to the `Error` component.
*
[`templates/website/src/blocks/Form/Number/index.tsx`](diffhunk://#diff-72e5bd63eda769bce077e87bc614cb338211600580ad38ba86a7f066a35212a5L33-R33):
Modified to pass the `name` prop to the `Error` component.
*
[`templates/website/src/blocks/Form/Select/index.tsx`](diffhunk://#diff-69d52ba3bb01fc0ce4428f5b76ab48a86c448dceaf36390edbcf345f0b15c34eL60-R60):
Modified to pass the `name` prop to the `Error` component.
*
[`templates/website/src/blocks/Form/State/index.tsx`](diffhunk://#diff-c0eb5a8c64b6384a44e19556556921bff4c89ed3a8d5a1d2e46ce493178587caL61-R61):
Modified to pass the `name` prop to the `Error` component.
*
[`templates/website/src/blocks/Form/Text/index.tsx`](diffhunk://#diff-9d32d5b3132729534809280d97d8a0952e96270f434b5d57a32a2d4981a36384L29-R29):
Modified to pass the `name` prop to the `Error` component.
*
[`templates/website/src/blocks/Form/Textarea/index.tsx`](diffhunk://#diff-d25c7cb831ee04c195983c1a88718bdcec8f1dc34c3e5237875678eb8194994dL37-R37):
Modified to pass the `name` prop to the `Error` component.
2025-03-10 12:22:07 +00:00
Sasha
3de1636e92 docs: document payload migrate:create flags (#11592)
Related discussion
https://github.com/payloadcms/payload/discussions/10978
2025-03-07 19:25:39 +02:00
Sasha
e9afb367b5 fix(db-mongodb): properly sanitize updateVersion read result (#11589)
Previously, `db.updateVersion` had a mistake with using `transform({
operation: 'write' })` instead of `transform({ operation: 'read' })`
which led to improper DB data sanitization (like ObjectID -> string,
Date -> string) when calling `payload.update` with `autosave: true` when
some other autosave draft already exists. This fixes
https://github.com/payloadcms/payload/issues/11542 additionally for this
case.
2025-03-07 19:14:02 +02:00
Jarrod Flesch
029cac3cd3 fix(graphql): sanitize graphql field names for schema generation (#11556)
### What? Cannot generate GraphQL schema with hyphenated field names
Using field names that do not adhere to the GraphQL `_a-z & A-Z`
standard prevent you from generating a schema, even though it will work
just fine everywhere else.

Example: `my-field-name` will prevent schema generation.

### How? Field name sanitization on generation and querying
This PR adds sanitization to the schema generation that sanitizes field
names.
- It formats field names in a GraphQL safe format for schema generation.
**It does not change your config.**
- It adds resolvers for field names that do not adhere so they can be
mapped from the config name to the GraphQL safe name.

Example:
- `my-field` will turn into `my_field` in the schema generation
- `my_field` will resolve from `my-field` when data comes out

### Other notes
- Moves code from `packages/graphql/src/schema/buildObjectType.ts` to
`packages/graphql/src/schema/fieldToSchemaMap.ts`
- Resolvers are only added when necessary: `if (formatName(field.name)
!== field.name)`.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-03-07 14:43:09 +00:00
Jessica Chowdhury
a53876d741 fix(ui): logic for showing copyToLocale button and adds test (#11584)
### What?
This [PR](https://github.com/payloadcms/payload/pull/11546) introduced a
bug where the `CopyToLocale` button can show up when localization is
false.

### Why?
`const disableCopyToLocale = localization &&
collectionConfig?.admin?.disableCopyToLocale` this line was faulty

### How?
Fixed the logic and added test to confirm button doesn't show when
localization is false.
2025-03-07 14:13:48 +00:00
Jessica Chowdhury
6f90d62fc2 fix(ui): upload.displayPreview should affect all previews in the admin panel (#11496)
### What?
We have the option to set `displayPreview: true || false` on upload
collections / upload fields - with the **field** option taking
precedence.

Currently, `displayPreview` is only affecting the list view for the
**_related_** collection.

i.e. if you go to a collection that has an upload field - the preview
will be hidden/shown correctly according to the `displayPreview` option.
<img width="620" alt="Screenshot 2025-03-03 at 12 38 18 PM"
src="https://github.com/user-attachments/assets/c11c2a84-0f64-4a08-940e-8c3f9096484b"
/>

However, when you go directly to the upload collection and look at the
list view - the preview is always shown, not affected by the
`displayPreview` option.
<img width="446" alt="Screenshot 2025-03-03 at 12 39 24 PM"
src="https://github.com/user-attachments/assets/f5e1267a-d98a-4c8c-8d54-93dea6cd2e31"
/>

Also, we have previews within the file field itself - also not being
affected by the `displayPreview` option.
<img width="528" alt="Screenshot 2025-03-03 at 12 40 06 PM"
src="https://github.com/user-attachments/assets/3dd04c9a-3d9f-4823-90f8-b538f3d420f9"
/>

All the upload related previews (excluding preview sizes and upload
editing options) should be affected by the `displayPreview` option.

### How?
Checks for `collection.displayPreview` and `field.displayPreview` in all
places where previews are displayed.

Closes #11404
2025-03-07 12:49:20 +00:00
Jessica Chowdhury
6699844d7b chore(ui): removes margin when row is empty and passes style from props (#11504)
Two small separate issues here (1) and (2):

### What?
1. Excess margin is displayed when a row is hidden due to
`admin.condition`
2. The `admin.style` props is never passed to the `row` field

### Why?
1. Unlike other fields, the `row` field still gets rendered when
`admin.condition` returns false - this is because the logic gets passed
down to the fields within the row
2. `style` was never being threaded to the `row` field wrapper

### How?
1. Hides the row using css to `display: none` when no children are
present
2. Passes `admin.styles` to the `row` wrapper

Fixes #11477
2025-03-07 12:48:58 +00:00
Jessica Chowdhury
657ad20278 feat(ui): adds disable copy to locale option to collection config (#11546)
### What?
Adds new option to disable the `copy to locale` button, adds description
to docs and adds e2e test.

### Why?
Client request.

### How?
The option can be used like this: 
```ts
// in collection config
  admin: {
    disableCopyToLocale: true,
  },
```
2025-03-07 12:48:08 +00:00
Elliot DeNolf
30af889e3b chore: set all licenses for internal tooling 2025-03-06 22:15:44 -05:00
Patrik
8378654fd0 fix(ui): apply consistent styling to custom & default block thumbnails (#11555)
Fixes #9744
2025-03-06 15:34:25 -05:00
Alessio Gravili
b0da85dfea chore(deps): bump next.js from 15.2.0 to 15.2.1 in monorepo (#11576)
This bumps next.js to 15.2.1 in our monorepo, guaranteeing compatibility
2025-03-06 19:09:33 +00:00
Jarrod Flesch
48115311e7 fix(ui): incorrect error states (#11574)
Fixes https://github.com/payloadcms/payload/issues/11568

### What? Out of sync errors states
- Collaspibles & Tabs were not reporting accurate child error counts
- Arrays could get into a state where they would not update their error
states
- Slight issue with toasts 

### Tabs & Collapsibles
The logic for determining matching field paths was not functioning as
intended. Fields were attempting to match with paths such as `_index-0`
which will not work.

### Arrays
The form state was not updating when the server sent back errorPaths.
This PR adds `errorPaths` to `serverPropsToAccept`.

### Toasts
Some toasts could report errors in the form of `my > > error`. This
ensures they will be `my > error`

### Misc
Removes 2 files that were not in use:
- `getFieldStateFromPaths.ts`
- `getNestedFieldState.ts`
2025-03-06 14:02:10 -05:00
Jacob Fletcher
7cef8900a7 chore(deps): bumps @payloadcms/admin-bar in templates and examples (#11566)
The Payload Admin Bar is now maintained in core and released under the
`@payloadcms` scope thanks to #3684. All templates and examples that
rely on this package now install from here and have been migrated
accordingly.
2025-03-06 12:09:32 -05:00
Alessio Gravili
557ac9931a feat(richtext-lexical): upgrade lexical from 0.21.0 to 0.27.1 (#11564)
Fixes https://github.com/payloadcms/payload/issues/10628

This upgrades lexical from 0.21.0 to 0.27.1. This will allow us to use the new node state API to implement custom text formats (e.g. text colors), [thanks to Germán](https://x.com/GermanJablo/status/1897345631821222292).

## Notable changes ported over from lexical playground:

### Table column freezing

https://github.com/user-attachments/assets/febdd7dd-6fa0-40d7-811c-9a38de04bfa7

### Block cursors

We now render a block cursor, which is a custom cursor that gets rendered when the browser doesn't render the native one. An example would be this this horizontal cursor above block nodes, if there is no space above:

![CleanShot 2025-03-05 at 18 48 08@2x](https://github.com/user-attachments/assets/f61ce280-599c-4123-bdf7-25507078fcd7)

Previously, those cursors were unstyled and not visible

### Table Alignment

Tables can now be aligned

![CleanShot 2025-03-05 at 19 48 32@2x](https://github.com/user-attachments/assets/3fe263db-a98e-4a5d-92fd-a0388e547e5b)
2025-03-06 17:06:39 +00:00
Elliot DeNolf
9f7e8f47d2 ci: adjust paths filter for workflows, only look at main.yml (#11572)
Refine the paths filter for workflows from `.github/workflows/**` to
`.github/workflows/main.yml`. This is the only workflow that affects the
build.
2025-03-06 15:16:57 +00:00
Elliot DeNolf
259ea6ab64 ci: add canary nightly cron, adjust lock and stale crons 2025-03-06 09:49:56 -05:00
Sasha
2ad035fb7b feat(db-mongodb): strip keys from the data that don't exist in the schema from read results (#11558)
This change makes so that data that exists in MongoDB but isn't defined
in the Payload config won't be included to `payload.find` /
`payload.db.find` calls. Now we strip all the additional keys.

Consider you have a field named `secretField` that's also `hidden: true`
(or `read: () => false`) that contains some sensitive data. Then you
removed this field from the database and as for now with the MongoDB
adapter this field will be included to the Local API / REST API results
without any consideration, as Payload doesn't know about it anymore.

This also fixes https://github.com/payloadcms/payload/issues/11542 if
you removed / renamed a relationship field from the schema, Payload
won't sanitize ObjectIDs back to strings anymore.

Ideally you should create a migration script that completely removes the
deleted field from the database with `$unset`, but people rarely do
this.

If you still need to keep those fields to the result, this PR allows you
to do this with the new `allowAdditionalKeys: true` flag.
2025-03-06 14:31:38 +00:00
Elliot DeNolf
1ad1de7a0d ci: use GITHUB_OUTPUT instead of set-output [skip ci] 2025-03-05 23:34:26 -05:00
Elliot DeNolf
179778223f ci: canary and internal releases [skip ci] (#11565)
- Adds support for numeric canary versions ie. `3.28.0-canary.0`,
subsequent prereleases will increment accordingly (like Next.js)
- _Our old way of doing canary releases_ is still available but will now
be tagged as `internal` ex. `3.28.0-internal.shorthash`
- Releases are triggered via workflow dispatch in Actions. Triggers off
of main will be released as `canary`, all others will be `internal`.
2025-03-05 23:19:01 -05:00
Alessio Gravili
1e708bdd12 feat(richtext-lexical): adds ability to disable auto link creation (#11563)
This adds a new `disableAutoLinks` property to the `LinkFeature` that lets you disable the automatic creation of links while typing them in the editor or pasting them.
2025-03-06 01:25:16 +00:00
Alessio Gravili
36921bd62b feat(richtext-lexical): new HTML converter (#11370)
Deprecates the old HTML converter and introduces a new one that functions similarly to our Lexical => JSX converter.
The old converter had the following limitations:

- It imported the entire lexical bundle
- It was challenging to implement. The sanitized lexical editor config had to be passed in as an argument, which was difficult to obtain
- It only worked on the server

This new HTML converter is lightweight, user-friendly, and works on both server and client. Instead of retrieving HTML converters from the editor config, they can be explicitly provided to the converter function.

By default, the converter expects populated data to function properly. If you need to use unpopulated data (e.g., when running it from a hook), you also have the option to use the async HTML converter, exported from `@payloadcms/richtext-lexical/html-async`, and provide a `populate` function - this function will then be used to dynamically populate nodes during the conversion process.

## Example 1 - generating HTML in your frontend

```tsx
'use client'

import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'

import React from 'react'

export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
  const html = convertLexicalToHTML({ data })

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```

## Example - converting Lexical Blocks

```tsx
'use client'

import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
  DefaultNodeTypes,
  SerializedBlockNode,
  SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'

import {
  convertLexicalToHTML,
  type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'

type NodeTypes =
  | DefaultNodeTypes
  | SerializedBlockNode<MyTextBlock>
  | SerializedInlineBlockNode<MyInlineBlock>

const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
  ...defaultConverters,
  blocks: {
    // Each key should match your block's slug
    myTextBlock: ({ node, providedCSSString }) =>
      `<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
  },
  inlineBlocks: {
    // Each key should match your inline block's slug
    myInlineBlock: ({ node, providedStyleTag }) =>
      `<span${providedStyleTag}>${node.fields.text}</span$>`,
  },
})

export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
  const html = convertLexicalToHTML({
    converters: htmlConverters,
    data,
  })

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```

## Example 3 - outputting HTML from the collection

```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
import type { MyTextBlock } from '@/payload-types.js'
import type { CollectionConfig } from 'payload'

import {
  BlocksFeature,
  type DefaultNodeTypes,
  lexicalEditor,
  lexicalHTMLField,
  type SerializedBlockNode,
} from '@payloadcms/richtext-lexical'

const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [
    {
      name: 'nameOfYourRichTextField',
      type: 'richText',
      editor: lexicalEditor(),
    },
    lexicalHTMLField({
      htmlFieldName: 'nameOfYourRichTextField_html',
      lexicalFieldName: 'nameOfYourRichTextField',
    }),
    {
      name: 'customRichText',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [
          ...defaultFeatures,
          BlocksFeature({
            blocks: [
              {
                interfaceName: 'MyTextBlock',
                slug: 'myTextBlock',
                fields: [
                  {
                    name: 'text',
                    type: 'text',
                  },
                ],
              },
            ],
          }),
        ],
      }),
    },
    lexicalHTMLField({
      htmlFieldName: 'customRichText_html',
      lexicalFieldName: 'customRichText',
      // can pass in additional converters or override default ones
      converters: (({ defaultConverters }) => ({
        ...defaultConverters,
        blocks: {
          myTextBlock: ({ node, providedCSSString }) =>
            `<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
        },
      })) as HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<MyTextBlock>>,
    }),
  ],
}
```
2025-03-06 00:13:56 +00:00
Alessio Gravili
3af0468062 fix: add missing auth property to new defaults function (#11561)
https://github.com/payloadcms/payload/pull/10794 added new defaults the config - however, these were only added to the deprecated `defaults` object, which caused our CI to fail. This PR adds them to the new `addDefaultsToConfig` function
2025-03-05 23:45:24 +00:00
James Mikrut
8f6d2e79a1 feat: allow specification of which JWT extraction methods are supported, and in which order (#10794)
This PR adds a top-level `auth` property to the Payload config, where
you can specify a new `jwtOrder` property to dictate, in Payload's local
auth strategy, which JWT extraction methods should be leveraged, and in
which order.

For example, we currently use incoming request headers to retrieve a JWT
in the following order:

1. If there is an `Authorization: JWT ${token}` header
2. If there is an `Authorization: Bearer ${token}` header
3. If there is an HTTP-only cookie with a token present

Now you can define which of these strategies you'd like to support, and
in which order.

Todo: 
- [ ] Docs
- [ ] Tests
2025-03-05 16:56:40 -05:00
Elliot DeNolf
54acdad190 chore(release): v3.27.0 [skip ci] 2025-03-05 16:44:09 -05:00
Sasha
312aa639b6 fix: safe auth strategy execution (#11515)
Previously when `authenticate` method from an authentication strategy
failed it stopped execution of the current request in
`createPayloadRequest` which isn't a good behavior.
Right now it completely prevents the admin panel from loading:
<img width="637" alt="image"
src="https://github.com/user-attachments/assets/7a6ca006-7457-4f9f-8746-7b3f52d65583"
/>

Now, each `strategy.authenticate` call is wrapped into `try` / `catch`,
if an error happens we use `logError` to correctly log that error by its
logging level.
2025-03-05 16:34:23 -05:00
Jarrod Flesch
2163b0fdb5 feat(ui): improves field error toast messages (#11521)
### What?
Adjusts how field errors are displayed within toasts so they are easier
to read.

![Frame 36
(1)](https://github.com/user-attachments/assets/3debec4f-8d78-42ef-84bc-efd574a63ac6)
2025-03-05 14:28:26 -05:00
Jacob Fletcher
9724067242 feat: payload admin bar (#3684)
Imports https://github.com/payloadcms/payload-admin-bar into the Payload
monorepo. This package will now be regularly maintained directly
alongside all Payload packages and now includes its own test suite.

A few changes minor have been made between v1.0.7 and latest:

1. The package name has changed from `payload-admin-bar` to
`@payloadcms/admin-bar`.

    ```diff
    - import { PayloadAdminBar } from 'payload-admin-bar'
    + import { PayloadAdminBar } from '@payloadcms/admin-bar'
    ```
2. The `collection` prop has been renamed to `collectionSlug`
3. The `authCollection` prop has been renamed to `authCollectionSlug`

Here's a screenshot of the admin bar in use within the Website Template:

<img width="1057" alt="Screenshot 2025-03-05 at 1 20 04 PM"
src="https://github.com/user-attachments/assets/2597a8fd-da75-4b2f-8979-4fc8132999e8"
/>

---------

Co-authored-by: Kalon Robson <kalon.robson@outlook.com>
2025-03-05 19:14:35 +00:00
Sasha
5cc0e74471 fix(storage-*): client uploads with disablePayloadAccessControl: true (#11530)
Fixes https://github.com/payloadcms/payload/issues/11473

Previously, when `disablePayloadAccessControl: true` was defined, client
uploads were working improperly. The reason is that
`addDataAndFileToRequest` expects `staticHandler` to be defined and we
don't add in case if `disablePayloadAccessControl: true`.

This PR makes it so otherwise and if we have `clientUploads`, it pushes
the "proxied" handler that responses only when the file was requested in
the context of client upload (from `addDataAndFileToRequest`)
2025-03-05 20:59:49 +02:00
Jarrod Flesch
6939a835ca fix: beforeValidate deleting value when access returns false (#11549)
### What?
Regression caused by https://github.com/payloadcms/payload/pull/11433 

If a beforeChange hook was checking for a missing or undefined `value`
in order to change the value before inserting into the database, data
could be lost.

### Why?
In #11433 the logic for setting the fallback field value was moved above
the logic that cleared the value when access control returned false.

### How?
This change ensures that the fallback value is passed into the
beforeValidate function _and_ still available with the fallback value on
siblingData if access control returns false.

Fixes https://github.com/payloadcms/payload/issues/11543
2025-03-05 13:34:08 -05:00
Paul
143b6e3b8e feat: allow hiding the blockName field visible in blocks' headers via admin.disableBlockName (#11301)
Adds a new `admin.disableBlockName` property that allows you to disable
the blockName field entirely in the admin view. It defaults to false for
backwards compatibility.
2025-03-05 18:24:39 +00:00
Germán Jabloñski
4ebe67324a fix(richtext-lexical): fix bug in $createAutoLinkNode when the link is preceded by a textnode (#11551)
If you type "hello www.world.com" the autlink would remove the word
"hello".
2025-03-05 18:06:24 +00:00
Said Akhrarov
bbfff30d41 docs: remove dead links from client live-preview (#11552)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR removes two links from a time where there was two distinct
live-preview examples. It also adjusts links for CORS and CSRF to a more
appropriate location in the docs.

### Why?
Now there's only the App Router example, so direct users there instead.

### How?
Changes to `docs/live-preview/client.mdx`
2025-03-05 12:54:20 -05:00
Patrik
ba30d7641f feat: threads path through field condition functions (#11528)
This PR updates the field `condition` function property to include a new
`path` argument.

The `path` arg provides the schema path of the field, including array
indices where applicable.

#### Changes:

- Added `path: (number | string)[]` in the Condition type.
- Updated relevant condition checks to ensure correct parameter usage.
2025-03-05 12:45:08 -05:00
Jacob Fletcher
04b046847b fix(ui): views rendered in drawers can update step nav (#11548)
When rendering views within a drawer outside of the edit view, i.e. from
the list view, it updates the underlying step nav to the collection of
the drawer. This is true for both document drawers and list drawers.

This is because the logic controlling this behavior relies on the
current edit depth, which is only incremented within the edit view
itself. Instead of doing this, we can conditionally run the setter
functions based the presence of a drawer slug.

An alternative to this would be to subscribe to the `drawerDepth`
context but this would be less efficient, as this requires an
unnecessary hook and subsequent rendering cycle.
2025-03-05 10:07:30 -05:00
Sasha
62c0872bbb test: add unit tests for getFieldByPath (#11533)
Adds unit tests for the new function `getFieldByPath` that was added
here https://github.com/payloadcms/payload/pull/11512
2025-03-05 14:21:24 +00:00
Sasha
31e217967e fix(ui): execute client upload handler only when file exists (#11538)
Fixes https://github.com/payloadcms/payload/issues/11537
2025-03-05 16:05:19 +02:00
Jacob Fletcher
8f203bbbe1 chore: cleanup generated configs (#11536)
Cleans up various Payload-generated configs, namely:
- Renames config entry files from `preferencesCollection.ts`,
`lockedDocumentsCollection.ts`, and `jobsCollection.ts` to `config.ts`
- Standardizes collection slugs for `payload-preferences`,
`payload-locked-documents`, and `payload-jobs` and reuses everywhere
- Renames camel-cased `payloadPreferences` directory to kebab case, i.e.
`payload-preferences`
2025-03-04 23:05:34 -05:00
Sasha
f0ea9185ef chore: indexes are not iterable, corrects indexes default value sanitization (#11534)
The PR https://github.com/payloadcms/payload/pull/11512 was merged
without changes from this PR
https://github.com/payloadcms/payload/pull/11524

Which caused all the tests to fail:
<img width="432" alt="image"
src="https://github.com/user-attachments/assets/7c19187e-a9c4-4dad-80c2-bdd6156eeb0b"
/>

Corrects default value sanitization with the new strategy from #11524
2025-03-05 02:23:03 +00:00
Dan Ribbens
672dace969 chore: add plugin-import-export to publishList release tool (#11535) 2025-03-04 21:18:35 -05:00
Sasha
e36ab6aa2a fix(storage-gcs): client uploads are enabled even if clientUploads is not set (#11527)
Client uploads were always enabled because a wrong variable was used,
when passing `enabled` to `initClientUploads`,
`gcsStorageOptions.enabled` instead of `gcsStorageOptions.clientUploads`

To enable client uploads with GCS you also additionally need to
configure CORS on Google Cloud, therefore this change breaks existing
logic
2025-03-05 03:11:54 +02:00
Sasha
bacc0f002a feat: compound indexes (#11512)
### What?
This PR adds ability to define indexes on several fields for collections
(compound indexes).

Example:
```ts
{
  indexes: [{ unique: true, fields: ['title', 'group.name'] }]
}
```

### Why?
This can be used to either speed up querying/sorting by 2 or more fields
at the same time or to ensure uniqueness between several fields.

### How?
Implements this logic in database adapters. Additionally, adds a utility
`getFieldByPath`.
2025-03-05 03:09:24 +02:00
Alessio Gravili
f01cfbcc57 feat: allows overriding import map location (#11532)
By default, Payload only attempts to locate the import map file in the following locations:

- `src/app/(payload)/{adminroute}/importMap.js`
- `app/(payload)/{adminroute}/importMap.js`

This is fine for most projects, but sometimes you may want to place the import map - or the Payload admin directory - somewhere else.

This PR adds a new `importMapFile` property that allows you to override this heuristic and specify your own import map path.
2025-03-05 01:07:29 +00:00
Dan Ribbens
4f822a439b feat: plugin-import-export initial work (#10795)
Adds new plugin-import-export initial version.

Allows for direct download and creation of downloadable collection data
stored to a json or csv uses the access control of the user creating the
request to make the file.

config options:
```ts
  /**
   * Collections to include the Import/Export controls in
   * Defaults to all collections
   */
  collections?: string[]
  /**
   * Enable to force the export to run synchronously
   */
  disableJobsQueue?: boolean
  /**
   * This function takes the default export collection configured in the plugin and allows you to override it by modifying and returning it
   * @param collection
   * @returns collection
   */
  overrideExportCollection?: (collection: CollectionOverride) => CollectionOverride

// payload.config.ts:
plugins: [
    importExportPlugin({
      collections: ['pages', 'users'],
      overrideExportCollection: (collection) => {
        collection.admin.group = 'System'
        collection.upload.staticDir = path.resolve(dirname, 'uploads')
        return collection
      },
      disableJobsQueue: true,
    }),
 ],
```

---------

Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
Co-authored-by: Kendell Joseph <kendelljoseph@gmail.com>
2025-03-05 01:06:43 +00:00
Alessio Gravili
cc05937633 fix(next): admin panel fails compiling when fullySpecified is set in next config (#11531)
If `experimental.fullySpecified` is set to `true` in the next config, the Payload admin panel fails to compile, throwing the following error:

```ts
Failed to compile.

../../node_modules/.pnpm/@payloadcms+next@3.25.0-canary.46647b4_@types+react@18.3.1_graphql@16.10.0_monaco-editor@0.40_w3ro7ziou6gzev7zbe3qqrwaqe/node_modules/@payloadcms/next/dist/views/Version/RenderFieldsToDiff/fields/Select/DiffViewer/index.js
Attempted import error: 'DiffMethod' is not exported from 'react-diff-viewer-continued' (imported as 'DiffMethod').
```

The issue stems from incorrect import statements in `react-diff-viewer-continued` 4.0.4. This was fixed in `react-diff-viewer-continued` 4.0.5.

This PR also enables `fullySpecified` in our test suites, to catch these issues going forward.
2025-03-05 00:04:03 +00:00
Elliot DeNolf
64b63f6833 ci: cache mongodb image to avoid rate limiting (#11529)
Adding usage of `ScribeMD/docker-cache` to cache the mongodb image.

We utilize the
[supercharge/mongodb-github-action](https://github.com/supercharge/mongodb-github-action)
for pulling and starting our mongo image. This would at times cause `You
have reached your unauthenticated pull rate limit` errors because of how
many jobs our CI spins up at one time.
2025-03-04 17:02:43 -05:00
Alessio Gravili
5adb764b08 fix: collection config deep merge during sanitization causing unpredictable behavior (#11524)
Deep‐merging the collection config defaults during sanitization causes all collection fields to end up with different object references. This is not only slow, but can also lead to unpredictable behavior: mutations made before collection sanitization are reflected in the field config, while mutations made afterward, using the same object reference, are not reflected in the collection’s field config.

Specifically, the following happened:

1. A Block was defined in the module scope.
2. It was then added to both a collection’s blocks field and the config.blocks property.
3. Rich text sanitization promises for config.blocks were collected.
4. The collection config was sanitized.
5. The config.blocks sanitization promises were awaited.
6. Rich text fields were sanitized in config.blocks, but ended up not being sanitized in the collection config referencing the same block, because the object reference held by the promise callback no longer matched the collection config’s object reference. The collection config block did not create its own rich text sanitization promise, as `_sanitized: true` was set on the block during the earlier config.blocks sanitization, which skipped it.

Our config defaults pattern was brittle in general. It’s easy to misuse object spreading or to mutate the config defaults later when you intended only to mutate the payload or collection config. Our current approach was vulnerable to this because it retained some object references from the config defaults.

This PR introduces reliable merge functions that are faster and ensure no object references are shared with defaults that reside in the module scope.
2025-03-04 21:02:26 +00:00
Jarrod Flesch
56dec13820 fix: format admin url inside forgot pw email (#11509)
### What?
Supersedes https://github.com/payloadcms/payload/pull/11490.

Refactors imports of `formatAdminURL` to import from `payload/shared`
instead of `@payloadcms/ui/shared`. The ui package now imports and
re-exports the function to prevent this from being a breaking change.

### Why?
This makes it easier for other packages/plugins to consume the
`formatAdminURL` function instead of needing to implement their own or
rely on the ui package for the utility.
2025-03-04 11:55:36 -05:00
Elliot DeNolf
1d168318d0 chore(release): v3.26.0 [skip ci] 2025-03-04 10:01:54 -05:00
Sasha
f143d25728 fix(storage-uploadthing): files are duplicated to the storage via client uploads (#11518)
When uploading file via client side upload we invalidate it then on the
server side with re-uploading. This works fine with most adapters since
they just replace the old file under the same key. UploadThing works
differently and generates a new key every time.

Example of the issue:
<img width="611" alt="image"
src="https://github.com/user-attachments/assets/9c01b52a-d159-4f32-9f66-3b5fbadab7b4"
/>

Now, we clear the old file before doing re-upload.
2025-03-04 14:57:30 +00:00
Patrik
7d2480aef9 fix(next): incorrect active state for partial matches of collection names in sidebar (#11511)
Previously, collections with similar names (e.g., `uploads` and
`uploads-poly`) both appeared active when viewing either collection.

This was due to `pathname.startsWith(href)`, which caused partial
matches.

This update refines the `isActive` logic to prevent partial matches.
2025-03-03 16:46:47 -05:00
Patrik
c417e3a627 fix: avif images not converting to upload.formatOptions set file types (#11505)
Previously, AVIF images were not converted to other file types as
expected, despite `upload.formatOptions` specifying a different file
type.

The issue was due to `canResizeImage` not recognizing `'image/avif',`
causing `fileSupportsResize` to return `false` and preventing the image
from undergoing format conversion.

This fix updates `canResizeImage` to include `'image/avif'`, ensuring
that AVIF images are processed correctly and converted to a different
file type when specified in `formatOptions`.

Fixes #10694 
Fixes #9985
2025-03-03 14:58:39 -05:00
Germán Jabloñski
efce1549d0 chore(plugin-search): enable TypeScript strict mode (#11508) 2025-03-03 18:31:26 +00:00
Ondřej Nývlt
d57a78616a docs: clarify that image resizing/cropping require sharp to be specified in payload config (#11470)
### What

Clarifies that `sharp` must be specified in payload config for image
resizing & cropping to work. Also adds link to the configuration page
for further information.

### Why

It is not immediately clear from this single documentation page alone.
While it says that the feature relies on sharp, it does not say that it
must be added to config. Most people won't probably run into this since
they're probably going to use `create-payload-app`, which configures
sharp by default. But those who use custom config (like me) may be left
wondering why this feature does not work.

See [Crop images and preview sizes not
working](https://payloadcms.com/community-help/discord/crop-images-and-preview-sizes-not-working)
in community help.
2025-03-03 13:24:05 -05:00
Germán Jabloñski
a3fe60778c chore(translations): enable TypeScript strict mode (#11494) 2025-03-03 13:01:14 -03:00
Jarrod Flesch
4ddf96502c fix(examples): ensure working multi-tenant example with pg (#11501)
### What?
There were a couple issues with the implementation within the example
when using postgres.
- `ensureUniqueUsername` tenant was being extracted incorrectly, should
not constrain query unless it was present
- `ensureUniqueSlug` was querying by NaN when tenant was not present on
data or originalDoc
- `users` read access was not correctly extracting the tenant id in the
correct type depending on DB

Fixes https://github.com/payloadcms/payload/issues/11484
2025-03-03 10:21:55 -05:00
Paul
562acb7492 templates: fix vercel website template importmap error caused by missing import (#11500)
The new client side handler was missing in the importmap on the template
for the vercel blob storage adapter
2025-03-03 15:15:44 +00:00
Jacob Fletcher
bf4fa59026 chore(deps): bumps payload-admin-bar to v1.0.7 to suppress react 19 warnings (#11499)
The `payload-admin-bar` now supports React 19 as a result of
https://github.com/payloadcms/payload-admin-bar/pull/13. This will
suppress the React 19 warnings on install within the website templates
and various examples that rely on this package.
2025-03-03 10:13:24 -05:00
Elliot DeNolf
fd1a4f689e ci: change custom github actions target back to es5 2025-03-03 10:08:13 -05:00
Patrik
a15c38f665 ci: clarify version reporting in issue templates (#11498)
This update improves the `Environment Info` section in the issue
template by asking users to provide exact version numbers instead of
"latest."

This ensures that bug reports remain accurate and useful over time.
2025-03-03 09:46:15 -05:00
Paul
fa8a2f8d8d chore: add docker volume directories to gitignore (#10902)
Added these directories to gitignore so they don't conflict with
stashing, which throws an error due to host user not having write
permissions
2025-03-03 11:32:40 +00:00
Vincent Vu
b9108b4306 docs: fix documentation "CheckListFeature" (#11480)
### What?
CheckListFeature is noted in the documentation. However, the package
uses ChecklistFeature

Rather than changing the package, this would be better.
2025-03-03 07:47:23 -03:00
Alessio Gravili
6a3d58bb32 feat(db-*): support limit in db.updateMany (#11488)
This PR adds a new `limit` property to `payload.db.updateMany`. This functionality is required for [migrating our job system to use faster, direct db adapter calls](https://github.com/payloadcms/payload/pull/11489)
2025-03-03 05:32:57 +00:00
Alessio Gravili
192964417d chore: temporarily disables flaky "should execute a custom script" test (#11487)
The newly added "should execute a custom script" int test is very flaky on mongodb - it was failing most of the time. This PR skips this test until it's fixed

Example failures:
- https://github.com/payloadcms/payload/actions/runs/13618762213/job/38065304540
- https://github.com/payloadcms/payload/actions/runs/13611742446/job/38049886588
- https://github.com/payloadcms/payload/actions/runs/13608918590/job/38043761182
- https://github.com/payloadcms/payload/actions/runs/13599001510/job/38021936623
2025-03-02 21:05:25 -07:00
Alessio Gravili
f03d450d8e templates: bump payload versions, upgrade next.js to 15.2.0, fix eslint errors (#11486)
- Ensures website templates build without eslint errors
- Upgrades all templates from Next.js 15.1.5 to 15.2.0
- Bumps all payload versions, updates all lockfiles to reference latest payload versions. The blank template was still installing 3.17.1 and the website template was installing 3.18.0
- Simplifies defaultLexical.ts
2025-03-03 02:01:53 +00:00
Alessio Gravili
398d48ab16 templates: improve naming of richtext component import, add 'payload-richtext' classname (#11485)
Our previous `RichTextWithoutBlocks` import alias was confusing - this PR changes it to `ConvertRichText`. This should make it clear that that's the imported RichText component that performs the editor state => JSX conversion
2025-03-02 18:34:34 -07:00
Alessio Gravili
377454416a chore(eslint): speed up no-imports-from-self rule by ensuring cache is used (#11483)
Our `no-imports-from-self` eslint rule was supposed to cache the package.json name to ensure it doesn't try to find and read the package.json for every single import statement.

Turns out that cache was never used. Credits to @etrepum for [finding this issue](https://github.com/facebook/lexical/pull/7272#discussion_r1976666227)
2025-03-02 19:51:49 +00:00
Alessio Gravili
cd29978faf feat(richtext-lexical): add htmlToLexical helper (#11479)
This adds a new `convertHTMLToLexical` helper that makes converting HTML to Lexical easy
2025-03-02 03:42:10 +00:00
Alessio Gravili
e1b30842fb feat(richtext-lexical): add editorConfigFactory helper to streamline getting the editor config (#11467)
This PR exports a new `editorConfigFactory` that provides multiple standardized ways to retrieve the editor configuration needed for the Lexical editor.

## Why this is needed

Getting the editor config is required for converting the lexical editor state into/from different formats, as it's needed to create a headless editor. While we're moving away from requiring headless editor instantiation for common format conversions, some conversion types and other use cases still require it.

Currently, retrieving the editor config is cumbersome - you either need an existing field to extract it from or the payload config to create it from scratch, with multiple approaches for each method.

## What this PR does

The `editorConfigFactory` consolidates all possible ways to retrieve the editor config into a single factory with clear methods:

```ts
editorConfigFactory.default()
editorConfigFactory.fromField()
editorConfigFactory.fromUnsanitizedField()
editorConfigFactory.fromFeatures()
editorConfigFactory.fromEditor()
```

This results in less code, simpler implementation, and improved developer experience. The PR also adds documentation for all retrieval methods.
2025-03-01 23:44:25 +00:00
Jacob Fletcher
927078c4db fix(ui): uses query provider as single source of truth for where builder (#11476)
The "where" builder maintains its own duplicative state for conditions.
This is problematic when an outside force needs to control the
conditions in some way, but the "where" builder will not receive those
updates.

While it is a requirement of the "where" builder to transform the
"where" query into "and" / "or" format for rendering, it does so in a
way that causes it to become out of sync with the query provider. This
is because we first initialize state from context, then for every change
to conditions, report those updates to contexts—but not the other way
around.

To fix this, we need to completely remove state from the "where" builder
and solely rely on the query context as a single source of truth. This
will allow it to receive automatic updates from query provider without
needing to sync both local state and context simultaneously. Now, we
only ever need to send updates to the query provider and let the
top-down rendering cycle propagate those changes everywhere.
2025-03-01 16:20:00 -05:00
Alessio Gravili
dda17f0c32 chore(richtext-lexical): export LexicalFieldAdminProps (#11464) 2025-03-01 03:32:02 +00:00
Alessio Gravili
6d8aca5ab3 fix(richtext-lexical): ensure nested forms do not use form element (#11462)
Previously, lexical blocks initialized a new `Form` component that rendered as `<form>` in the DOM. This may lead to React errors, as forms nested within forms is not valid HTML.

This PR changes them to render as `<div>` in the DOM instead.
2025-02-28 19:29:07 -07:00
Dmitrii Kuzmin
c828e336ee fix: excludes index files from migration files filtering (#10722) 2025-02-28 19:39:44 -05:00
Sasha
90d3c65008 perf: automatically add index on a relationship field when used as target for a join field (#11463)
When the join field is used, Payload now automatically adds an index on
the target relationship field.

For example:
```
{
  name: 'relatedPosts',
  type: 'join',
  collection: "posts",
  on: 'category',
},

{
  name: 'category',
  type: 'relationship',
  relationTo: "categories",
},
```

Here, `index: true` implicitly added to the `category` relationship
field during sanitization to improve querying performance.
2025-03-01 02:37:19 +02:00
Said Akhrarov
e75d38ca82 fix(ui): remove stale thumbnails in bulkUpload after partial success (#10651) 2025-02-28 22:18:18 +00:00
Sasha
79a7b4ad02 chore(db-mongodb): tsconfig uses strict: true and noUncheckedIndexedAccess: true (#11444)
Migrates the `db-mongodb` package to use `strict: true` and
`noUncheckedIndexedAccess: true` TSConfig properties.
This greatly improves code quality and prevents some runtime errors or
gives better error messages.
2025-03-01 00:17:24 +02:00
Vincent Vu
f7f5651004 fix(templates): handle enableLink condition safely (#10728) 2025-02-28 22:03:42 +00:00
Michael Bykovski
45a7c8b764 fix: response headers from authstrategies are now merged together, even if no user was returned (#10883)
### What?
Merges response headers returned from auth strategies

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-02-28 16:38:32 -05:00
Sasha
25e8799a09 docs: add few notes about DocumentDB and Azure Cosmos DB (#11336)
Adds few notes about limitations when using Azure Cosmos DB and
DocumentDB

Discussion: https://github.com/payloadcms/payload/discussions/11333
2025-02-28 21:14:51 +00:00
Jacob Fletcher
6cbda9e231 fix(ui): bulk editing users throws client-side exception (#11461)
When bulk editing an auth-enabled collection such as users, a
client-side exception is thrown. This is because we're trying to access
the `disableBulkEdit` property on `undefined`. This is due to hidden,
auth-specific fields like `salt` and `hash` lacking an admin config.

No test is explicitly needed for this as `"strictNullChecks": true` will
throw an error at compile time, once enabled.
2025-02-28 21:06:40 +00:00
Sasha
fc42c40883 fix(storage-s3, storage-azure, storage-gcs): client uploads when a collection has prefix configured (#11436)
### What?
Fixes client uploads when storage collection config has the `prefix`
property configured. Previously, it failed with "Object key was not
found".

### Why?
This is expected to work.

### How?
The client upload handler now receives to its props `prefix`. Then it
threads it to the server-side `staticHandler` through
`clientUploadContext` and then to `getFilePrefix`, which checks for
`clientUploadContext.prefix` and returns if there is.

Previously, `staticHandler` tried to load the file without including
prefix consideration.

This changes only these adapters:
* S3
* Azure
* GCS

With the Vercel Blob adapter, `prefix` works correctly.
2025-02-28 15:50:23 -05:00
Patrik
83b4548fc1 fix(next): active nav item not clickable in edit view (#11457)
This fixes an issue where the active collection nav item was
non-clickable inside documents. Now, it remains clickable when viewing a
document, allowing users to return to the list view from the nav items
in the sidebar.

The active state indicator still appears in both cases.
2025-02-28 15:14:21 -05:00
Jarrod Flesch
81e8a9d50d chore(examples): update multi-tenant example (#11459)
Bumps deps inside examples repo lockfile. Fixes import map, supersedes
https://github.com/payloadcms/payload/pull/10804
2025-02-28 14:57:58 -05:00
Mike Newberry
9bb89b7b52 feat(ui): close the nav when the user navigates away on small screens (#10932) 2025-02-28 19:31:57 +00:00
Sasha
d4d2bf4617 perf(db-mongodb): faster join field aggregation by replacing mongoose-aggregate-paginate-v2 with a custom implementation (#10936)
Fixes
https://github.com/payloadcms/payload/discussions/10165#discussioncomment-12034047

As described in the discussion, we have an incorrect order of
aggregation pipeline when using aggregations with the join field. We
must use `$sort`, `$skip`, `$limit` before the `$lookup` or otherwise
mongodb scans all the docs, applies `$lookup` for them and only after
applies `$limit`, `$skip`.
Replaces `mongoose-aggregate-paginate-v2` with a custom
`aggregatePaginate` because we need a custom solution here. This was
considered in https://github.com/payloadcms/payload/pull/9594 but it was
reverted as for now.

Fixes https://github.com/payloadcms/payload/issues/11187
2025-02-28 21:30:00 +02:00
nomad-dev
8b5bc3de33 docs: fix method useAllFormFields on admin/hooks.mdx (#10935) 2025-02-28 19:27:27 +00:00
Liège Arthur
19b4ec2562 docs: use local API to upload a local file (#10839) 2025-02-28 19:18:54 +00:00
Said Akhrarov
bef98c8d6e fix(ui): scope rah-static and progress-bar styles to payload-default layer (#11442) 2025-02-28 14:05:39 -05:00
Said Akhrarov
77395b6483 docs: adds info and example for headersWithCors (#11141) 2025-02-28 13:43:00 -05:00
Violet Rosenzweig
fcaf59176d docs: custom auth strategy requires the collection slug in return value (#11327) 2025-02-28 13:30:29 -05:00
Adrian Maj
206b4b9d88 docs: broken &apos; char entity instead of ' in plugins/build-your-own (#11363) 2025-02-28 13:27:40 -05:00
Jacob Fletcher
67c4a20237 fix(next): properly instantiates req.url on localhost (#11455)
The `req.url` property at the page level was not reflective of the
actual URL on localhost. This was because we were passing an
incompatible `url` override into `createLocalReq` (lacking protocol).
This would silently fail to construct the URL object, ultimately losing
the top-level domain on `req.url` as well as the port on `req.origin`
(see #11454).

Closes #11448.
2025-02-28 13:04:26 -05:00
Alessio Gravili
38131ed2c3 feat: ability to cancel jobs (#11409)
This adds new `payload.jobs.cancel` and `payload.jobs.cancelByID` methods that allow you to cancel already-running jobs, or prevent queued jobs from running.

While it's not possible to cancel a function mid-execution, this will stop job execution the next time the job makes a request to the db, which happens after every task.
2025-02-28 17:58:43 +00:00
Patrik
96d1d90e78 fix(ui): use full image url for upload previews instead of thumbnail url (#11435) 2025-02-28 12:47:02 -05:00
Jessica Chowdhury
9e97319c6f fix(ui): locale selector in versions view should remove filtered locales (#11447)
### What?
The `locale selector` in the version comparison view shows all locales
on first load. It does not accomodate the `filterAvailableLocales`
option and shows locales which should be filtered.

### How?
Pass the initial locales through the `filterAvailableLocales` function.

Closes #11408

#### Testing
Use test suite `localization` and the `localized-drafts` collection.
Test added to `test/localization/e2e`.
2025-02-28 17:37:07 +00:00
Jacob Fletcher
a65289c211 fix: ensures req.origin includes port on localhost (#11454)
The `req.origin` property on the `PayloadRequest` object does not
include the port when running on localhost, a requirement of the [HTML
Living Standard](https://html.spec.whatwg.org/#origin). This was because
we were initializing the url with a fallback of `http://localhost` (no
port). When constructed via `new URL()`, the port is unable to be
extracted. This is fixed by using the `host` property off the headers
object, if it exists, which includes the port.

Partial fix for #11448.
2025-02-28 12:26:38 -05:00
Alessio Gravili
4a1b74952f chore(richtext-lexical): improve UploadData jsdocs (#11292) 2025-02-28 12:18:09 -05:00
Martijn Luyckx
8b55e7b51a docs: replace HTML entity &apos; with literal apostrophe (#11321) 2025-02-28 17:15:00 +00:00
Roy Barber
48e613b61f fix(examples): replace depreciated 'mergeHeaders' import in the MultiTenant example (#11306) 2025-02-28 17:13:46 +00:00
Jessica Chowdhury
428c133033 fix(ui): copyToLocale should not pass id in data, throws error in postgres (#11402) 2025-02-28 12:06:32 -05:00
Alessio Gravili
c8c578f5ef perf(next): reduce initReq calls from 3 to 1 per page load (#11312)
This PR significantly improves performance when navigating through the admin panel by reducing the number of times `initReq` is called. Previously, `initReq`—which handles expensive tasks like initializing Payload and running access control—was called **three times** for a single page load (for the root layout, the root page, and the notFound page).

We initially tried to use React Cache to ensure `initReq` only ran once per request. However, because React Cache performs a shallow object reference check on function arguments, the configuration object we passed (`configPromise`) and the `overrides` object never maintained the same reference, causing the cache to miss.

### What’s Changed

*   **New `getInitReqContainer` Helper**  
    We introduced a helper that provides a stable object reference throughout the entire request. This allows React to properly cache the output, ensuring `initReq` doesn’t get triggered multiple times by mistake.
    
*   **Splitting `initReq` into Two Functions**  
    The `initReq` logic was split into:
    
    *   **`initPartialReq`:** Runs only **once** per request, handling tasks that do not depend on page-level data (e.g., calling `.auth`, which performs a DB request).
    *    **`initReq`:** Runs **twice** (once for Layout+NotFound page and once for main page), handling tasks, most notably access control, that rely on page-level data such as locale or query parameters. The NotFound page will share the same req as the layout page, as it's not localized, and its access control wouldn't need to access page query / url / locale, just like the layout.

* **Remove duplicative logic**
   * Previously, a lot of logic was run in **both** `initReq` **and** the respective page / layout. This was completely unnecessary, as `initReq` was already running that logic. This PR returns the calculated variables from `initReq`, so they don't have to be duplicatively calculated again.

### Performance Gains

*   Previously:
    *   `.auth` call ran **3 times**
    *   Access control ran **3 times**
*   Now:
    *   `.auth` call runs **1 time**
    *   Access control runs **2 times**

This change yields a noticeable performance improvement by cutting down on redundant work.
2025-02-28 09:25:03 -07:00
Alessio Gravili
d53f166476 fix: ensure errors returned from tasks are properly logged (#11443)
Fixes https://github.com/payloadcms/payload/issues/9767

We allow failing a job queue task by returning `{ state: 'failed' }` from the task, instead of throwing an error. However, previously, this threw an error when trying to update the task in the database. Additionally, it was not possible to customize the error message.

This PR fixes that by letting you return `errorMessage` alongside `{ state: 'failed' }`, and by ensuring the error is transformed into proper json before saving it to the `error` column.
2025-02-28 16:00:56 +00:00
Sasha
dfddee2125 fix(storage-*): ensure client handler is always added to the import map, even if the plugin is disabled (#11438)
Ensures that even if you pass `enabled: false` to the storage adapter
options, e.g:
```ts
s3Storage({
  enabled: false,
  collections: {
    [mediaSlug]: true,
  },
  bucket: process.env.S3_BUCKET,
  config: {
    credentials: {
      accessKeyId: process.env.S3_ACCESS_KEY_ID,
      secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
    },
  },
})
```
the client handler component is added to the import map. This prevents
errors when you use the adapter only on production, but you don't
regenerate the import map before running the build
2025-02-28 16:34:00 +02:00
Patrik
e055565ca8 ci: repro guide to use create-payload-app@latest instead of @beta (#11451)
This PR updates the reproduction guide to reference
`create-payload-app@latest -t blank` instead of `@beta`, ensuring users
follow the latest stable release when setting up a minimal reproduction.
2025-02-28 09:32:09 -05:00
Alessio Gravili
41c7413f59 feat(db-*): add updateMany method to database adapter (#11441)
This PR adds a new `payload.db.updateMany` method, which is a more performant way to update multiple documents compared to using `payload.update`.
2025-02-27 20:30:17 -07:00
Jacob Fletcher
3709950d50 feat: maintains column state in url (#11387)
Maintains column state in the URL. This makes it possible to share
direct links to the list view in a specific column order or active
column state, similar to the behavior of filters. This also makes it
possible to change both the filters and columns in the same rendering
cycle, a requirement of the "list presets" feature being worked on here:
#11330.

For example:

```
?columns=%5B"title"%2C"content"%2C"-updatedAt"%2C"createdAt"%2C"id"%5D
```

The `-` prefix denotes that the column is inactive.

This strategy performs a single round trip to the server, ultimately
simplifying the table columns provider as it no longer needs to request
a newly rendered table for itself. Without this change, column state
would need to be replaced first, followed by a change to the filters.
This would make an unnecessary number of requests to the server and
briefly render the UI in a stale state.

This all happens behind an optimistic update, where the state of the
columns is immediately reflected in the UI while the request takes place
in the background.

Technically speaking, an additional database query in performed compared
to the old strategy, whereas before we'd send the data through the
request to avoid this. But this is a necessary tradeoff and doesn't have
huge performance implications. One could argue that this is actually a
good thing, as the data might have changed in the background which would
not have been reflected in the result otherwise.
2025-02-27 20:00:40 -05:00
Alessio Gravili
6ce5e8b83b perf: disable returning of db operations that don't need the return value (#11437)
If the return value of a db operation is not used, we can pass `returning: false` which will result in the query being executed faster.

See https://github.com/payloadcms/payload/pull/11393
2025-02-28 00:53:23 +00:00
Philipp Meyer
f3844ee533 fix(next): email verification not working due to incorrect token url parsing (#11439)
### What?
This PR reverts a presumably accidental change made in
[b80010b1a1](b80010b1a1),
that broke the email verification feature in v3.24.0 and onwards.
### Why?
Through the missing verify in `const [collectionSlug, verify, token] =
params.segments`, the token value was always the string `verify`
2025-02-28 00:05:05 +00:00
Alessio Gravili
c21dac1b53 perf(db-*): add option to disable returning modified documents in db methods (#11393)
This PR adds a new `returning` option to various db adapter methods. Setting it to `false` where the return value is not used will lead to performance gains, as we don't have to do additional db calls to fetch the updated document and then sanitize it.
2025-02-27 17:40:22 -05:00
Jarrod Flesch
b3e7a9d194 fix: incorrect value inside beforeValidate field hooks (#11433)
### What?
`value` within the beforeValidate field hook was not correctly falling
back to the document value when no value was passed inside the request
for the field.

### Why?
The fallback logic was running after the beforeValidate field hooks are
called.

### How?
Run the fallback logic before running the beforeValidate field hooks.

Fixes https://github.com/payloadcms/payload/issues/10923
2025-02-27 16:00:27 -05:00
Jacob Fletcher
c4bc0ae48a fix(next): disables active nav item (#11434)
When visiting a collection's list view, the nav item corresponding to
that collection correctly appears in an active state, but is still
rendered as an anchor tag. This makes it possible to reload the current
page by simply clicking the link, which is a problem because this
performs an unnecessary server roundtrip. This is especially apparent
when search params exist in the current URL, as the href on the link
does not.

Unrelated: also cleans up leftover code that was missed in this PR:
#11155
2025-02-27 15:21:28 -05:00
Patrik
f7b1cd9d63 fix(ui): duplicate basePath in Logout Button Link (#11432)
This PR resolves an issue where the `href` for the Logout button in the
admin panel included duplicate `basePath` values when `basePath` was set
in `next.config.js`.

The Logout button was recently updated to use `NextLink` (`next/link`),
which automatically applies the `basePath` from the Next.js
configuration. As a result, manually adding the `basePath` to the `href`
is no longer necessary.

Relevant PRs that modified this behavior originally: 
- #9275
- #11155
2025-02-27 13:58:27 -05:00
Jarrod Flesch
9c25e7b68e fix(plugin-multi-tenant): scope access constraint to admin collection (#11430)
### What?
The idea of this plugin is to only add constraints when a user is
present on a request. This change makes it so access control only
applies to admin panel users as they are the ones assigned to tenants.

This change allows you to more freely write access functions on tenant
enabled collections. Say you have 2 auth enabled collections, the plugin
would incorrectly assume since there is a user on the req that it needs
to apply tenant constraints. When really, you should be able to just add
in your own access check for `req.user.collection` and return true/false
if you want to prevent/allow other auth enabled collections for certain
operations.

```ts
import { Access } from 'payload'

const readByTenant: Access = ({ req }) => {
  const { user } = req
  if (!user || user.collection === 'auth2') return false
  return true
}
```

When you have a function like this that returns `true` and the
collection is multi-tenant enabled - the plugin injects constraints
ensuring the user on the request is assigned to the tenant on the doc
being accessed.

Before this change, you would need to opt out of access control with
`useTenantAccess` and then wire up your own access function:

```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'

export const tenantAccess: Access = async ({ req: { user } }) => {
  if (user) {
    if (user.collection === 'auth2') {
      return true
    }

    // Before, you would need to re-implement
    // internal multi-tenant access constraints
    if (user.roles?.includes('super-admin')) return true

    return getTenantAccess({
      fieldName: 'tenant',
      user,
    })
  }

  return false
}
```

After this change you would not need to opt out of `useTenantAccess` and
can just write:

```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'

export const tenantAccess: Access = async ({ req: { user } }) => {
  return Boolean(user)
}
```

This is because internally the plugin will only add the tenant
constraint when the access function returns true/Where _AND_ the user
belongs to the admin panel users collection.
2025-02-27 13:53:45 -05:00
Elliot DeNolf
1d252cbacf templates: bump for v3.25.0 (#11431)
🤖 Automated bump of templates for v3.25.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-27 13:13:47 -05:00
Alessio Gravili
7118b6418f fix(ui): disable publish button if form is autosaving (#11343)
Fixes https://github.com/payloadcms/payload/issues/6648

This PR introduces a new `useFormBackgroundProcessing` hook and a corresponding `setBackgroundProcessing` function in the `useForm` hook.

Unlike `useFormProcessing` / `setProcessing`, which mark the entire form as read-only, this new approach only disables the Publish button during autosaving, keeping form fields editable for a better user experience.

I named it `backgroundProcessing` because it should run behind the scenes without disrupting the user. You could argue that it is a bit more generic than something like `isAutosaving`, but it signals intent: Background = do not disrupt the user.
2025-02-27 10:28:08 -07:00
Elliot DeNolf
bdf0113b2f chore(release): v3.25.0 [skip ci] 2025-02-27 12:06:03 -05:00
Sasha
3436fb16ea feat: allow to count related docs for join fields (#11395)
### What?
For the join field query adds ability to specify `count: true`, example:
```ts
const result = await payload.find({
  joins: {
    'group.relatedPosts': {
      sort: '-title',
      count: true,
    },
  },
  collection: "categories",
})

result.group?.relatedPosts?.totalDocs // available
```

### Why?
Can be useful to implement full pagination / show total related
documents count in the UI.

### How?
Implements the logic in database adapters. In MongoDB it's additional
`$lookup` that has `$count` in the pipeline. In SQL, it's additional
subquery with `COUNT(*)`. Preserves the current behavior by default,
since counting introduces overhead.


Additionally, fixes a typescript generation error for join fields.
Before, `docs` and `hasNextPage` were marked as nullable, which is not
true, these fields cannot be `null`.
Additionally, fixes threading of `joinQuery` in
`transform/read/traverseFields` for group / tab fields recursive calls.
2025-02-27 16:05:48 +00:00
Patrik
bcc68572bf docs: adds Reserved Field Names section to migration guide (#11308)
Added a new Reserved Field Names section to the migration guide.

Clarified that certain field names (`__v`, `salt`, `hash`, `file`, etc.)
are reserved for internal use and will be sanitized from the config if
used.

Included additional reserved names specific to `MongoDB`, `auth`-enabled
collections, and `upload`-enabled collections.

Added a note recommending against using field names with an underscore
(`_`) prefix, as they are reserved for internal columns and may cause
conflicts in `SQL` and other contexts.

Fixes #11159
2025-02-27 10:36:34 -05:00
Jacob Fletcher
6aa9da73f8 Revert "feat: simplify column prefs (#11390)" (#11427)
This reverts commit 69c0d09 in #11390.

In order to future proof column prefs, it probably is best to continue
to use the current shape. This change was intended to ensure that as
little transformation to URL params was made as possible for #11387, but
we will likely transform them after all.

This will ensure that we can add support for additional properties over
time, as needed. For example, if we hypothetically wanted to add a
custom `label` or similar feature to columns prefs, it would make more
sense to use explicit properties to identity `accessor` and `active`.

For example:

```ts
[
  {
    accessor: "title",
    active: true,
    label: 'Custom Label' // hypothetical
  }
]
```
2025-02-27 08:39:24 -05:00
Alessio Gravili
2a3682ff68 fix(deps): ensure Next.js 15.2.0 compatibility, upgrade nextjs and @types/react versions in monorepo (#11419)
This bumps next.js to 15.2.0 in our monorepo, as well as all @types/react and @types/react-dom versions. Additionally, it removes the obsolete `peerDependencies` property from our root package.json.

This PR also fixes 2 bugs introduced by Next.js 15.2.0. This highlights why running our test suite against the latest Next.js, to make sure Payload is compatible, version is important.

## 1. handleWhereChange running endlessly

Upgrading to Next.js 15.2.0 caused `handleWhereChange` to be continuously called by a `useEffect` when the list view filters were opened, leading to a React error - I did not investigate why upgrading the Next.js version caused that, but this PR fixes it by making use of the more predictable `useEffectEvent`.

## 2. Custom Block and Array label React key errors

Upgrading to Next.js 15.2.0 caused react key errors when rendering custom block and array row labels on the server. This has been fixed by rendering those with a key

## 3. Table React key errors

When rendering a `Table`, a React key error is thrown since Next.js 15.2.0
2025-02-27 05:56:09 +00:00
Jarrod Flesch
958e195017 feat(plugin-multi-tenant): allow customization of selector label (#11418)
### What?
Allows for custom labeling of the tenant selector shown in the sidebar.

Fixes https://github.com/payloadcms/payload/issues/11262
2025-02-26 22:39:51 -05:00
Jarrod Flesch
45cee23add feat(plugin-multi-tenant): filter users list and tenants lists (#11417)
### What?
- Adds `users` base list filtering when tenant is selected
- Adds `tenants` base list filtering when tenant is selected
2025-02-26 21:50:36 -05:00
Alessio Gravili
67b7a730ba docs: improve lexical code block documentation (#11416)
The existing code example had type errors when `strict: true` was enabled
2025-02-27 00:47:36 +00:00
Alessio Gravili
88a2841500 docs: add lexical docs for configuring jsx converters for internal links and overriding them (#11415)
This PR improves existing JSX converter docs and adds 2 new sections:
- **converting internal links** - addresses why a `"found internal link, but internalDocToHref is not provided"` error is thrown, and how to get around it
- **Overriding default JSX Converters**
2025-02-27 00:41:41 +00:00
Alessio Gravili
7e713a454a feat(richtext-lexical): allows client features to access components added to the import map by the server feature (#11414)
Lexical server features are able to add components to the import map through the `componentImports` property. As of now, the client feature did not have access to those. This is usually not necessary, as those import map entries are used internally to render custom components server-side, e.g. when a request to the form state endpoint is made.

However, in some cases, these import map entries need to be accessed by the client feature (see "Why" section below).

This PR ensures that keyed `componentImports` entries are made available to the client feature via the new `featureClientImportMap` property.

## Why?

This is a prerequisite of the lexical [wrapper blocks PR](https://github.com/payloadcms/payload/pull/9289), where wrapper block custom components need to be made to the ClientFeature. The ClientFeature is where the wrapper block node is registered - in order to generate the wrapper block node, we need access to the component
2025-02-27 00:21:15 +00:00
Jacob Fletcher
b975858e76 test: removes all unnecessary page.waitForURL methods (#11412)
Removes all unnecessary `page.waitForURL` methods within e2e tests.
These are unneeded when following a `page.goto` call because the
subsequent page load is already being awaited.

It is only a requirement when:

- Clicking a link and expecting navigation
- Expecting a redirect after a route change
- Waiting for a change in search params
2025-02-26 16:54:39 -05:00
Sasha
b540da53ec feat(storage-*): large file uploads on Vercel (#11382)
Currently, usage of Payload on Vercel has a limitation - uploads are
limited by 4.5MB file size.
This PR allows you to pass `clientUploads: true` to all existing storage
adapters
* Storage S3
* Vercel Blob
* Google Cloud Storage
* Uploadthing
* Azure Blob

And then, Payload will do uploads on the client instead. With the S3
Adapter it uses signed URLs and with Vercel Blob it does this -
https://vercel.com/guides/how-to-bypass-vercel-body-size-limit-serverless-functions#step-2:-create-a-client-upload-route.
Note that it doesn't mean that anyone can now upload files to your
storage, it still does auth checks and you can customize that with
`clientUploads.access`


https://github.com/user-attachments/assets/5083c76c-8f5a-43dc-a88c-9ddc4527d91c

Implements https://github.com/payloadcms/payload/discussions/7569
feature request.
2025-02-26 21:59:34 +02:00
Alessio Gravili
c6ab312286 chore: cleanup queues test suite (#11410)
This PR extracts each workflow of our queues test suite into its own file
2025-02-26 19:43:31 +00:00
Sasha
526e535763 fix: ensure custom IDs are returned to the result when select query exists (#11400)
Previously, behavior with custom IDs and `select` query was incorrect.
By default, the `id` field is guaranteed to be selected, even if it
doesn't exist in the `select` query, this wasn't true for custom IDs.
2025-02-26 17:05:50 +02:00
Alessio Gravili
e4712a822b perf(drizzle): use faster, direct db query for getting id to update in updateOne (#11391)
Previously, `updateOne` was using `buildFindManyArgs` and `findFirst` just to retrieve the ID of the document to update, which is a huge function that's not necessary to run just to get the document ID.

This PR refactors it to use a simple `db.select` query to retrieve the ID
2025-02-25 21:54:38 -07:00
Patrik
81fd42ef69 fix(ui): skip bulk upload thumbnail generation on non-image files (#11378)
This PR fixes an issue where bulk upload attempts to generate thumbnails
for non-image files, causing errors on the page.

The fix ensures that thumbnail generation is skipped for non-image
files, preventing unnecessary errors.

Fixes #10428
2025-02-25 16:55:44 -05:00
Sasha
6b6c289d79 fix(db-mongodb): hasNextPage with polymorphic joins (#11394)
Previously, `hasNextPage` was working incorrectly with polymorphic joins
(that have an array of `collection`) in MongoDB.

This PR fixes it and adds extra assertions to the polymorphic joins
test.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-02-25 21:22:47 +00:00
Jacob Fletcher
69c0d09437 feat: simplify column prefs (#11390)
Transforms how column prefs are stored in the database. This change
reduces the complexity of the `columns` property by removing the
unnecessary `accessor` and `active` keys.

This change is necessary in order to [maintain column state in the
URL](https://github.com/payloadcms/payload/pull/11387), where the state
itself needs to be as concise as possible. Does so in a non-breaking
way, where the old column shape is transformed as needed.

Here's an example:

Before:

```ts
[
  {
    accessor: "title",
    active: true
  }
]
```

After:

```ts
[
  {
    title: true
  }
]
```
2025-02-25 15:18:14 -05:00
Elliot DeNolf
48f183bd42 ci: remove codeowners file (#11385)
Since codeowner approvals are not currently required, the codeowners
file is only serving to add reviewers to PRs.

Removing the codeowners file for now as this is not desired. Can be
re-introduced at a later date if required approvers are implemented.
2025-02-25 10:06:14 -05:00
Jarrod Flesch
36e152d69d fix(ui): allow json fields to be updated externally (#11371)
### What?
Unable to update json fields externally. For example, calling `setValue`
on a json field would not be reflected in the admin panel UI.

### Why?
JSON fields use the monaco editor to manage state internally, so
programmatically updating the value in state does not change the
internal value.

### How?
Set a ref when the user updates the value and then unset the ref after
the change is complete.

Inside the hook that watches `value`, if the value changed and the
change came from the system (i.e. a programmatic change) refresh the
editor by adjusting its key prop. If the change was made by the user,
there is no need to refresh the editor.

Fixes https://github.com/payloadcms/payload/issues/10819
2025-02-25 09:44:06 -05:00
Kendell Joseph
1e698c2bdf chore(plugin-cloud): refresh session on ExpiredToken error code (#8904)
Fixes https://github.com/payloadcms/payload/issues/8404

This code will refresh the session token upon receiving an
`ExpiredToken` error when `storageClient.getObject()` receives that
error.

- The `Error Code` detected is determined by the error reported in the
issue, and is an actual code from [AWS
documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html)
for Error Responses.

- If the request fails again, the error is thrown.
2025-02-25 09:17:30 -05:00
Patrik
7bb1c9d3c6 fix(ui): bulk upload DiscardWithoutSaving modal styles (#11381)
This PR fixes an issue where the `DiscardWithoutSaving` modal in the
bulk upload process was missing its styles.

The modal now displays correctly with the intended styling.

### Before:
![Screenshot 2025-02-24 at 8 20
52 PM](https://github.com/user-attachments/assets/c83a3119-28ce-4701-bc64-1219adeb2505)

### After:
![Screenshot 2025-02-24 at 8 20
07 PM](https://github.com/user-attachments/assets/62d364c2-b64c-4bd9-bf05-7481d609c6e4)

Fixes #11380
2025-02-25 08:56:42 -05:00
Alessio Gravili
4410a49132 refactor: simplify collection, global and auth operations (#11374)
Continuation of https://github.com/payloadcms/payload/pull/11372 but for our collection, global and auth operations

Previously, we were quite frequently using `.reduce()` to run hooks. This PR replaces them with simple `for` loops, which is less overhead, less code, less confusing and simpler to understand.
2025-02-24 20:50:25 +00:00
Alessio Gravili
820a6ec55d fix: ensure generated types for config.blocks are not undefined if no blocks defined (#11377)
Previously, if no `config.blocks` were defined, `blocks: undefined` would incorrectly be added to the generated types.
2025-02-24 20:33:22 +00:00
Jacob Fletcher
0a1af45549 fix(next): nested relationship filter options (#11375)
Continuation of #11008. When `filterOptions` are set on a relationship
field that is _nested within another field_, those filter options are
not applied to `Filter` component in the list view. This is because we
were only shallowly resolving filter options on top-level fields, as
opposed to recursively traversing fields to resolve them even when
deeply nested.
2025-02-24 15:24:25 -05:00
Patrik
09ca5143eb fix(plugin-nested-docs): fallback to empty string if useAsTitle field is undefined (#11338)
Updated `formatBreadcrumb` to fall back to an empty string if the
`useAsTitle` field for the document is undefined.

This handles cases where the field is optional or not filled out,
ensuring the label is never `undefined`.

Fixes #10377
2025-02-24 14:00:41 -05:00
Patrik
f1b005c4f5 fix(ui): object type field labels not displaying in search filter (#11366)
This PR ensures that when `titleField.label` is provided as an object,
it is correctly translated and displayed in the search filter's
placeholder.

Previously, the implementation only supported string values, which could
lead to issues with object type labels. With these changes, object type
labels will now properly show as intended in the search filter.

Fixes #11348
2025-02-24 13:38:21 -05:00
Alessio Gravili
dc9e8fa655 refactor: simplify running field hooks (#11372)
Previously, we were quite frequently using `.reduce()` to sequentially run field hooks. This PR replaces them with simple `for` loops, which is less overhead, less code, less confusing and simpler to understand.

Additionally, it refactors `mergeLocaleActions` which previously was unnecessarily complex. They no longer entail async code, thus we no longer have to juggle with promises
2025-02-24 18:37:33 +00:00
Jacob Fletcher
2477fc6c75 fix(ui): custom block labels stale when reordering blocks (#11367)
When blocks have custom row labels, those row labels become stale when
reordering blocks. After moving a block, for example, the row label will
jump back the original block until form state returns with the proper
rendering order. This is especially evident on slow networks.
2025-02-24 16:52:12 +00:00
Jarrod Flesch
37781808eb fix(plugin-multi-tenant): user access, thread field names through (#11365)
### What?
Two things:
1. Users unassigned to a tenant could not access their own account
2. Custom `tenantsArrayFieldName` and `tenantsArrayTenantFieldName`
configurations were not being used in all cases

### Why?
1. The access constraint provided by the plugin would not allow them to
make changes to their own account
2. `getUserTenantIDs` and `afterTenantDelete` were not using the custom
field names properly

### How?
1. Adds constraint for users allowing them to manage their own account
by default. Externally nothing has changed. If you need to lock your
users access control down you should do that just as you would without
this plugin.
2. Threads the field names through for usage.

Fixes https://github.com/payloadcms/payload/issues/11317
2025-02-24 11:17:09 -05:00
Jessica Chowdhury
d92c0009ed fix(plugin-nested-docs): corrects data shape of breadcrumbs returned in hooks (#10866)
### What?
The `plugin-nested-docs` returns an array of breadcrumbs - the
`resaveChildren` file accidentally processed the breadcrumbs twice, once
where the data is updated and once within the `populateBreadcrumbs`
function which was causing the objects to be double nested.

### How?
Removes the extra nesting from `resaveChildren` file and allows the
`populateBreadcrumbs` to return the final data.

Fixes #10855
2025-02-24 11:10:21 -05:00
Rafal Sypien
d32608649c docs: add info about changes in localized fields to v2 -> v3 migration guide (#11244)
### What?
Information that locale fields in database are changing to a simpler
data structure in v3.

### Why?
Simple data migration is not enough to get it working, I had to spend
quite some time to figure out migration files and still it required some
manual input. Maybe others will find it useful when starting a v3
migration.

### How?
I had to do some custom migration scripts on my own to get v3 to work
with existing pages data.

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2025-02-24 17:36:11 +02:00
Paul
f9121c1a3a fix(ui): link element triggering clicks twice (#11362)
Fixes
https://github.com/payloadcms/payload/issues/11359#issuecomment-2678213414

The link element by using startTransitionRoute and manually calling
router.push would technically cause links to be clicked twice, by not
preventing default browser behaviour.

This caused a problem on clicking /create links as it hit the route
twice. Added a test making sure Create new doesn't lead to abnormally
increased document counts

Changes:
- Added `e.preventDefault()` in our Link element
- Added `preventDefault` as an optional prop to this element so that
people can handle it on their own if needed via a custom `onClick`
2025-02-24 14:49:52 +00:00
Jarrod Flesch
f477e0e3c4 feat(plugin-multi-tenant): export useTenantSelection hook for public usage (#11364)
Exports the `useTenantSelection` hook from the multi-tenant plugin, this
way other users can import and use the hook along with it's methods.

Can be imported:
```ts
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
```

The context returned:

```ts
type ContextType = {
  /**
   * Array of options to select from
   */
  options: OptionObject[]
  /**
   * The currently selected tenant ID
   */
  selectedTenantID: number | string | undefined
  /**
   * Prevents a refresh when the tenant is changed
   *
   * If not switching tenants while viewing a "global", set to true
   */
  setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
  /**
   * Sets the selected tenant ID
   * 
   * @param args.id - The ID of the tenant to select
   * @param args.refresh - Whether to refresh the page after changing the tenant
   */
  setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
}
```
2025-02-24 09:29:45 -05:00
Sasha
4224c68002 docs: fix links to react hooks (#11344) 2025-02-22 13:23:00 +00:00
Paul
b014416584 feat: add support for not_like operation (#11326)
This PR adds support for `not_like` as a query operation. Functions just
as a negative to `like`, uses `ilike` in postgres and `like` in sqlite
2025-02-22 00:32:37 +00:00
Alessio Gravili
1725af5e3a fix(next): use correct hmr url if assetPrefix is set in next config (#10859)
The next.js assetPrefix needs to be included in the websocket URL.

Previously, we were appending both assetPrefix and basePath, which is incorrect. `assetPrefix` overrides `basePath` if both are set. This PR mimics the way Next.js connects to the HMR server.

Sources:
- https://github.com/AlessioGr/next.js/blob/canary/packages/next/src/server/lib/router-server.ts#L688
- https://github.com/AlessioGr/next.js/blob/canary/packages/next/src/server/config.ts#L322
- https://github.com/AlessioGr/next.js/blob/canary/packages/next/src/client/components/react-dev-overlay/app/client-entry.tsx
2025-02-22 00:27:07 +00:00
Sasha
a13d4fe5c6 perf: remove deep copy of the entire sanitized entity config in configToJSONSchema (#11342)
Small performance improvement for types generation and anywhere else
`configToJSONSchema` can be used.

We did a deep copy of the whole sanitized entity config, which can be
expensive. Now, to do the needed mutation of `flattenedFields`, we just
create a new reference of `flattenedFields` for that.
2025-02-22 01:59:50 +02:00
Alessio Gravili
3ffc268438 fix: safely access config.blocks during type generation (#11320)
A discord user reported a "`TypeError: config.blocks is not iterable`" error when generating types: https://discord.com/channels/967097582721572934/1342383112289517578/1342383112289517578

To prevent that error, this PR ensures config.blocks is safely accessed during type generation
2025-02-21 23:26:28 +00:00
Jacob Fletcher
d766b1904c feat(ui): threads row data through list drawer onSelect callback (#11339)
When rendering a list drawer, you can pass a custom `onSelect` callback
to execute when the user clicks on the linked cell within the table. The
underlying handler, however, only passes the `docID` and
`collectionSlug` args through the callback, rather than the document
itself. This makes it impossible to perform side-effects that require
the data of the row that was selected.

Instances of this callback were also largely untyped.

Needed for #11330.
2025-02-21 17:08:05 -05:00
Jacob Fletcher
f31568c69c refactor(ui): moves bulk edit controls (#11332)
Bulk edit controls are currently displayed within the search bar of the
list view. This doesn't make sense from a UX perspective, as the current
selection is displayed somewhere else entirely. These controls also take
up a lot of visual real estate which is beginning to get overused
especially after the introduction of "list menu items" in #11230, and
the potential introduction of "saved filters" controls in #11330.

Now, they are rendered contextually _alongside_ the selection count. To
make room for these new controls, they are displayed in plain text and
the entity labels have been removed from the selection count.
2025-02-21 15:06:30 -05:00
Jacob Fletcher
a8bec9a1b2 fix(ui): only show bulk select all when count is less than total (#11329)
It doesn't make sense to display "select all" when bulk editing if your
current selection already contains all records.
2025-02-21 13:42:47 -05:00
Patrik
5d7be15c52 templates: update docker-compose to use postgres by default in postgres specific templates (#11328)
### What?

Updated `docker-compose.yml` to use `Postgres` by default, with
`MongoDB` commented out in the templates that are by default using the
postgres adapter.

Fixes #11322
2025-02-21 13:15:54 -05:00
Alessio Gravili
0058f82d87 perf: add limit: 1 and pagination: false to various payload queries (#11319)
`payload.find` queries can be made faster by specifying `limit: 1` and `pagination: false` when only the first document is needed. This PR applies those options to various queries to improve performance.
2025-02-21 11:09:40 -07:00
Tib
6ff380ce59 docs: update outdated docs/rich-text/overview feature names (#11324)
Features was outdated
2025-02-21 10:29:57 -07:00
Sasha
f779e48a58 fix: working bin configuration for custom scripts (#11294)
Previously, the `bin` configuration wasn't working at all.
Possibly because in an ESM environment this cannot work, because
`import` always returns an object with a default export under the
`module` key.
```ts
const script: BinScript = await import(pathToFileURL(userBinScript.scriptPath).toString())
await script(config)
```
Now, this works, but you must define a `script` export from your file.
Attached an integration test that asserts that it actually works. Added
documentation on how to use it, as previously it was missing.
This can be also helpful for plugins.

### Documentation

Using the `bin` configuration property, you can inject your own scripts
to `npx payload`.
Example for `pnpm payload seed`:

Step 1: create `seed.ts` file in the same folder with
`payload.config.ts` with:

```ts
import type { SanitizedConfig } from 'payload'

import payload from 'payload'

// Script must define a "script" function export that accepts the sanitized config
export const script = async (config: SanitizedConfig) => {
  await payload.init({ config })
  await payload.create({ collection: 'pages', data: { title: 'my title' } })
  payload.logger.info('Succesffully seeded!')
  process.exit(0)
}
```

Step 2: add the `seed` script to `bin`:
```ts
export default buildConfig({
  bin: [
    {
      scriptPath: path.resolve(dirname, 'seed.ts'),
      key: 'seed',
    },
  ],
})
```

Now you can run the script using:
```sh
pnpm payload seed
```
2025-02-21 18:15:27 +02:00
Sasha
1dc748d341 perf(db-mongodb): remove JSON.parse(JSON.stringify) copying of results (#11293)
Improves performance and optimizes memory usage for mongodb adapter by
cutting down copying of results via `JSON.parse(JSON.stringify())`.
Instead, `transform` does necessary transformations (`ObjectID` ->
`string,` `Date` -> `string`) without any copying
2025-02-21 17:31:24 +02:00
Alessio Gravili
c7c5018675 perf(next): reduce getNavPrefs calls from 3 to 1 per page load (#11318)
Previously, we were calling `getNavPrefs` (a payload.find call) three times for every single page load.

This PR:

1. Ensures that `getNavPrefs` is called only once per page load, reducing two unnecessary `payload.find` calls every time a page is loaded or navigated to.
2. Adds `pagination: false` to the `payload.find` call, making it more efficient and improving performance.

## How?

We were using React's cache to ensure that navigation preferences (`getNavPrefs`) were fetched only once per request. However, this wasn't working as expected because the first argument of `getNavPrefs` was an object. Each time it was called, a new object reference was passed, preventing React from caching it properly.

To fix this, this PR ensures that only primitive values are used as arguments for caching, following best practices and making the cache function work as intended.
2025-02-21 05:17:39 +00:00
Alessio Gravili
9728d80592 fix: db transaction errors caused by checkDocumentLockStatus (#11287)
Just like https://github.com/payloadcms/payload/pull/11269, we stop passing through `req` to db operations in `checkDocumentLockStatus`.

After extensive testing, this seems to get rid of all transaction errors that occurred when I was testing autosave against a remote mongo DB.

We keep the `req` for postgres, as it mysteriously breaks in CI - this cannot be reproduced locally
2025-02-20 20:01:15 -07:00
Sasha
0594701004 chore: add JSDoc for globals Local API operations (#11313)
Continuation of https://github.com/payloadcms/payload/pull/11265 for
globals.
Documents all the possible properties of globals Local API operations
with JSDoc.
2025-02-21 04:33:36 +02:00
Alessio Gravili
845c647ebc perf: ensure fetching and updating preferences doesn't cause transaction errors and is done correctly (#11311)
## getPreferences function caching

Our `getPreferences` function used in the ui package is now wrapped in react cache, to minimize the amount of times it runs on a single request. This mimics the behavior of our other `getPreferences` function in the next package.

## getPreferences  incorrect behavior

The `getPreferences` function in the next package was passing through the incorrect user slug. This would not have been noticeable in projects with just one users collection, but might break in projects with multiple users collections.

## getPreferences performance optimization

This PR adds `pagination: false` to the getPreferences payload.find() call, which will speed up the query.

## upsertPreferences transaction errors

Due to the potential of preference upsert operations running in parallel (e.g. when switching locales), this PR disables transactions in the preferences creation / update calls. This fixes the transaction errors reported in https://github.com/payloadcms/payload/issues/11310
2025-02-21 01:53:56 +00:00
Sasha
f6f6a1dc99 chore: cut down logging noise when file was not found on the disk (#11295)
When `/uploads/file/file-path.jpg` endpoint is requested, and
`file-path.jpg` _was found_ in the database, but was not found on the
disk, the error like this is printed to the console:

```
[03:34:54] ERROR: ENOENT: no such file or directory, stat '/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png'
    err: {
      "type": "Error",
      "message": "ENOENT: no such file or directory, stat '/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png'",
      "stack":
          Error: ENOENT: no such file or directory, stat '/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png'
              at async Object.stat (node:internal/fs/promises:1037:18)
              at async getFileHandler (webpack-internal:///(rsc)/./packages/payload/src/uploads/endpoints/getFile.ts:59:19)
              at async handleEndpoints (webpack-internal:///(rsc)/./packages/payload/src/utilities/handleEndpoints.ts:178:26)
              at async eval (webpack-internal:///(rsc)/./packages/next/src/routes/rest/index.ts:27:26)
              at async AppRouteRouteModule.do (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:10:32847)
              at async AppRouteRouteModule.handle (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:10:39868)
              at async doRender (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:1452:42)
              at async responseGenerator (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:1822:28)
              at async DevServer.renderToResponseWithComponentsImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:1832:28)
              at async DevServer.renderPageComponent (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:2259:24)
              at async DevServer.renderToResponseImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:2297:32)
              at async DevServer.pipeImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:959:25)
              at async NextNodeServer.handleCatchallRenderRequest (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/next-server.js:281:17)
              at async DevServer.handleRequestImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:853:17)
              at async /Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/dev/next-dev-server.js:371:20
              at async Span.traceAsyncFn (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/trace/trace.js:153:20)
              at async DevServer.handleRequest (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/dev/next-dev-server.js:368:24)
              at async invokeRender (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/lib/router-server.js:230:21)
              at async handleRequest (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/lib/router-server.js:408:24)
              at async NextCustomServer.requestHandlerImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/lib/router-server.js:432:13)
              at async Server.<anonymous> (file:///Users/sasha/work/payload/test/dev.ts:1:2848)
      "errno": -2,
      "code": "ENOENT",
      "syscall": "stat",
      "path": "/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png"
    }
```
Which is quite noisy as we understand why this error happens (and it
might be intentional when you load production DB to local and don't have
any files)

Now, the logging here in case of `ENOENT`  is simplified to this:
```
[03:43:35] ERROR: File Screenshot 2025-02-19 at 18.50.46.png for collection uploads-1 is missing on the disk. Expected path: /Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png
```


Fixes https://github.com/payloadcms/payload/issues/6246
2025-02-21 02:05:10 +02:00
Sasha
ee5e96a965 chore: add JSDoc for collection Local API operations properties (#11265)
### What?
Adds JSDoc for options of all collection Local API operations
_Every_ property now is documented there, even those that aren't on our
website docs.

### Why?
This is useful to have, now you can hover over any property to see what
it does in your editor.
Some properties also link to the documentation website directly for more
info.

### How?
Updates every collection operation arguments definition with JSDoc.

For globals will be a separate PR.
2025-02-21 02:04:52 +02:00
Said Akhrarov
22f61ad79e feat(ui): adds support for block groups (#11239)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR introduces support for the `admin.group` property in block
configs. This property enables blocks to be grouped under a common,
potentially localized, label in the block drawer component. This makes
it easier to sort through large collections of blocks. Previously, all
blocks would be in one common layout.

This PR also encompasses documentation changes and e2e tests to check
for the rendering of group labels.

### Why?
To make it easier to organize many blocks in block fields.

### How?
By introducing a new `admin.group` property in block configs and
assembling them in the blocks drawer component.

Before:

![Editing-Block-Field-Payload--before](https://github.com/user-attachments/assets/fb0c887b-ee47-46a1-a249-c4a4b7a5c13c)

After:

![Editing-Block-Field-Payload--after](https://github.com/user-attachments/assets/046d5a6f-3108-4464-ac69-8b7afcf27094)

Demo:

[Editing---Block-Field---Payload-groups-demo.webm](https://github.com/user-attachments/assets/2b351dc1-0d14-4a5b-ae71-bcd31fbb23df)

Addresses #5609
2025-02-20 19:51:47 +00:00
Jarrod Flesch
460d50baa3 fix(ui): confirmation modal should build off of drawerDepth, instead … (#11305)
When the new ConfirmationModal is used outside of the context of an
EditDepthProvider, it stacks behind currently open modals. This is
apparent when using it in custom views.

The fix is to build the confirm modal's depth off of drawerDepth instead
of editDepth.
2025-02-20 14:40:29 -05:00
Alessio Gravili
76bd05cc5d perf(next): avoid unnecessary upsertPreferences call on page load (#11302)
`getRequestLocale` => `upsertPreferences` is already called as part of `initReq`, yet we were still unnecessarily calling `getRequestLocale` afterwards, which potentially resulted in at least one unnecessary `payload.find()` or `payload.update()` call.
2025-02-20 19:00:31 +00:00
1036 changed files with 39047 additions and 25085 deletions

37
.github/CODEOWNERS vendored
View File

@@ -1,37 +0,0 @@
# Order matters. The last matching pattern takes precedence.
### Package Exports
**/exports/ @denolfe @jmikrut @DanRibbens
### Packages
/packages/plugin-cloud*/src/ @denolfe @jmikrut @DanRibbens
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
/packages/live-preview*/src/ @jacobsfletch
/packages/plugin-stripe/src/ @jacobsfletch
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
/packages/richtext-*/src/ @AlessioGr
/packages/next/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/ui/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Templates
/templates/_data/ @denolfe @jmikrut @DanRibbens
/templates/_template/ @denolfe @jmikrut @DanRibbens
### Build Files
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
### Root
/package.json @denolfe @jmikrut @DanRibbens
/tools/ @denolfe @jmikrut @DanRibbens
/.husky/ @denolfe @jmikrut @DanRibbens
/.vscode/ @denolfe @jmikrut @DanRibbens @AlessioGr
/.github/ @denolfe @jmikrut @DanRibbens

View File

@@ -57,7 +57,7 @@ body:
- type: textarea
attributes:
label: Environment Info
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions.
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
render: text
placeholder: |
Payload:

View File

@@ -20,7 +20,7 @@ body:
- type: textarea
attributes:
label: Environment Info
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions.
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
render: text
placeholder: |
Payload:

View File

@@ -0,0 +1,27 @@
MIT License
Copyright (c) 2020-2025 Cameron Little <cameron@camlittle.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
Modifications made by Payload CMS, Inc. <info@payloadcms.com>, 2025
Details in README.md

View File

@@ -4,6 +4,7 @@
"private": true,
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
"license": "MIT",
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
"main": "dist/index.js",
"scripts": {
"build": "pnpm build:typecheck && pnpm build:ncc",

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2022",
"target": "es5",
"lib": ["es2020.string"],
"noEmit": true,
"strict": true,

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2022",
"target": "es5",
"lib": ["es2020.string"],
"noEmit": true,
"strict": true,

View File

@@ -4,7 +4,7 @@ Depending on the quality of reproduction steps, this issue may be closed if no r
### Why was this issue marked with the `invalid-reproduction` label?
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@latest -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.

View File

@@ -2,8 +2,8 @@ name: lock-issues
on:
schedule:
# Run nightly at 12am EST
- cron: '0 4 * * *'
# Run nightly at 12am EST, staggered with stale workflow
- cron: '0 5 * * *'
workflow_dispatch:
permissions:

View File

@@ -41,14 +41,14 @@ jobs:
with:
filters: |
needs_build:
- '.github/workflows/**'
- '.github/workflows/main.yml'
- 'packages/**'
- 'test/**'
- 'pnpm-lock.yaml'
- 'package.json'
- 'templates/**'
needs_tests:
- '.github/workflows/**'
- '.github/workflows/main.yml'
- 'packages/**'
- 'test/**'
- 'pnpm-lock.yaml'
@@ -275,6 +275,7 @@ jobs:
- admin__e2e__general
- admin__e2e__list-view
- admin__e2e__document-view
- admin-bar
- admin-root
- auth
- auth-basic
@@ -307,12 +308,14 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- form-state
- live-preview
- localization
- locked-documents
- i18n
- plugin-cloud-storage
- plugin-form-builder
- plugin-import-export
- plugin-nested-docs
- plugin-seo
- versions
@@ -417,6 +420,7 @@ jobs:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: payloadtests
MONGODB_VERSION: 6.0
steps:
- uses: actions/checkout@v4
@@ -456,8 +460,14 @@ jobs:
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
if: matrix.database == 'postgres'
# Avoid dockerhub rate-limiting
- name: Cache Docker images
uses: ScribeMD/docker-cache@0.5.0
with:
key: docker-${{ runner.os }}-mongo-${{ env.MONGODB_VERSION }}
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.11.0
uses: supercharge/mongodb-github-action@1.12.0
with:
mongodb-version: 6.0
if: matrix.database == 'mongodb'

View File

@@ -53,6 +53,7 @@ jobs:
plugin-cloud
plugin-cloud-storage
plugin-form-builder
plugin-import-export
plugin-multi-tenant
plugin-nested-docs
plugin-redirects

View File

@@ -1,6 +1,9 @@
name: release-canary
name: publish-prerelease
on:
schedule:
# Run nightly at 10pm EST
- cron: '0 3 * * *'
workflow_dispatch:
env:
@@ -11,7 +14,7 @@ env:
jobs:
release:
name: release-canary-${{ github.ref_name }}-${{ github.sha }}
name: publish-prerelease-${{ github.ref_name }}-${{ github.sha }}
permissions:
id-token: write
runs-on: ubuntu-24.04
@@ -27,8 +30,19 @@ jobs:
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Canary release script
run: pnpm release:canary
- name: Determine release type
id: determine_release_type
# Use 'canary' for main branch, 'internal' for others
run: |
if [[ ${{ github.ref_name }} == "main" ]]; then
echo "release_type=canary" >> $GITHUB_OUTPUT
else
echo "release_type=internal" >> $GITHUB_OUTPUT
fi
- name: Release
run: pnpm publish-prerelease --tag ${{ steps.determine_release_type.outputs.release_type }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true

View File

@@ -2,8 +2,8 @@ name: stale
on:
schedule:
# Run nightly at 1am EST
- cron: '0 5 * * *'
# Run nightly at 1am EST, staggered with lock-issues workflow
- cron: '0 6 * * *'
workflow_dispatch:
inputs:

5
.gitignore vendored
View File

@@ -306,6 +306,8 @@ $RECYCLE.BIN/
/build
.swc
app/(payload)/admin/importMap.js
test/admin-bar/app/(payload)/admin/importMap.js
/test/admin-bar/app/(payload)/admin/importMap.js
test/live-preview/app/(payload)/admin/importMap.js
/test/live-preview/app/(payload)/admin/importMap.js
test/admin-root/app/(payload)/admin/importMap.js
@@ -318,3 +320,6 @@ test/databaseAdapter.js
/media-with-relation-preview
/media-without-relation-preview
/media-without-cache-tags
test/.localstack
test/google-cloud-storage
test/azurestoragedata/

3
.idea/payload.iml generated
View File

@@ -80,8 +80,9 @@
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-import-export/dist" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
</module>

View File

@@ -6,7 +6,11 @@ desc: Customize the metadata of your pages within the Admin Panel
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more, without any additional configuration. This includes the page title, description, og:image and everything in between. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views, allowing for the ability to control metadata on any page with high precision.
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more. This includes the page title, description, og:image, etc. and requires no additional configuration.
Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views. This allows for the ability to control metadata on any page with high precision, while also providing sensible defaults.
All metadata is injected into Next.js' [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function. This used to generate the `<head>` of pages within the Admin Panel. All metadata options that are available in Next.js are exposed by Payload.
Within the Admin Panel, metadata can be customized at the following levels:
@@ -48,13 +52,9 @@ The following options are available for Root Metadata:
| Key | Type | Description |
| --- | --- | --- |
| **`title`** | `string` | The title of the Admin Panel. |
| **`description`** | `string` | The description of the Admin Panel. |
| **`defaultOGImageType`** | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
| **`icons`** | `IconConfig[]` | An array of icon objects. [More details](#icons) |
| **`keywords`** | `string` | A comma-separated list of keywords to include in the metadata of the Admin Panel. |
| **`openGraph`** | `OpenGraphConfig` | An object containing Open Graph metadata. [More details](#open-graph) |
| **`titleSuffix`** | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
| `defaultOGImageType` | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
| `titleSuffix` | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
| `[keyof Metadata]` | `unknown` | Any other properties that Next.js supports within the `generateMetadata` function. [More details](https://nextjs.org/docs/app/api-reference/functions/generate-metadata). |
<Banner type="success">
**Reminder:**
@@ -67,7 +67,7 @@ The Icons Config corresponds to the `<link>` tags that are used to specify icons
The most common icon type is the favicon, which is displayed in the browser tab. This is specified by the `rel` attribute `icon`. Other common icon types include `apple-touch-icon`, which is used by Apple devices when the Admin Panel is saved to the home screen, and `mask-icon`, which is used by Safari to mask the Admin Panel icon.
To customize icons, use the `icons` key within the `admin.meta` object in your Payload Config:
To customize icons, use the `admin.meta.icons` property in your Payload Config:
```ts
{
@@ -93,23 +93,13 @@ To customize icons, use the `icons` key within the `admin.meta` object in your P
}
```
The following options are available for Icons:
| Key | Type | Description |
| --- | --- | --- |
| **`rel`** | `string` | The HTML `rel` attribute of the icon. |
| **`type`** | `string` | The MIME type of the icon. |
| **`color`** | `string` | The color of the icon. |
| **`fetchPriority`** | `string` | The [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) of the icon. |
| **`media`** | `string` | The [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) of the icon. |
| **`sizes`** | `string` | The [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) of the icon. |
| **`url`** | `string` | The URL pointing the resource of the icon. |
For a full list of all available Icon options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons).
### Open Graph
Open Graph metadata is a set of tags that are used to control how URLs are displayed when shared on social media platforms. Open Graph metadata is automatically generated by Payload, but can be customized at the Root level.
To customize Open Graph metadata, use the `openGraph` key within the `admin.meta` object in your Payload Config:
To customize Open Graph metadata, use the `admin.meta.openGraph` property in your Payload Config:
```ts
{
@@ -135,14 +125,46 @@ To customize Open Graph metadata, use the `openGraph` key within the `admin.meta
}
```
The following options are available for Open Graph Metadata:
For a full list of all available Open Graph options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#opengraph).
| Key | Type | Description |
| --- | --- | --- |
| **`description`** | `string` | The description of the Admin Panel. |
| **`images`** | `OGImageConfig` or `OGImageConfig[]` | An array of image objects. |
| **`siteName`** | `string` | The name of the site. |
| **`title`** | `string` | The title of the Admin Panel. |
### Robots
Setting the `robots` property will allow you to control the `robots` meta tag that is rendered within the `<head>` of the Admin Panel. This can be used to control how search engines index pages and displays them in search results.
By default, the Admin Panel is set to prevent search engines from indexing pages within the Admin Panel.
To customize the Robots Config, use the `admin.meta.robots` property in your Payload Config:
```ts
{
// ...
admin: {
meta: {
// highlight-start
robots: 'noindex, nofollow',
// highlight-end
},
},
}
```
For a full list of all available Robots options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#robots).
##### Prevent Crawling
While setting meta tags via `admin.meta.robots` can prevent search engines from _indexing_ web pages, it does not prevent them from being _crawled_.
To prevent your pages from being crawled altogether, add a `robots.txt` file to your root directory.
```text
User-agent: *
Disallow: /admin/
```
<Banner type="info">
**Note:**
If you've customized the path to your Admin Panel via `config.routes`, be sure to update the `Disallow` directive to match your custom path.
</Banner>
## Collection Metadata

View File

@@ -135,7 +135,7 @@ const ExampleComponent: React.FC = () => {
#### Updating other fields' values
If you are building a Custom Component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useFormFields`.
If you are building a Custom Component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useAllFormFields`.
You can send the following actions to the `dispatchFields` function.

View File

@@ -62,9 +62,12 @@ export const Users: CollectionConfig = {
})
return {
// Send the user back to authenticate,
// Send the user with the collection slug back to authenticate,
// or send null if no user should be authenticated
user: usersQuery.docs[0] || null,
user: usersQuery.docs[0] ? {
collection: 'users'
...usersQuery.docs[0],
} : null,
// Optionally, you can return headers
// that you'd like Payload to set here when

View File

@@ -57,9 +57,9 @@ export const Posts: CollectionConfig = {
The following options are available:
| Option | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| Option | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
@@ -67,17 +67,18 @@ The following options are available:
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
_* An asterisk denotes that a property is required._
@@ -120,6 +121,7 @@ The following options are available:
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
| `description` | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this Collection's List View. |
| `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |

View File

@@ -188,13 +188,15 @@ In order to use [Custom Translations](#custom-translations) in your project, you
Here we create a shareable translations object. We will import this in both our custom components and in our Payload config.
In this example we show how to extend English, but you can do the same for any language you want.
```ts
// <rootDir>/custom-translations.ts
import type { Config } from 'payload'
import { enTranslations } from '@payloadcms/translations/languages/en'
import type { NestedKeysStripped } from '@payloadcms/translations'
export const customTranslations: Config['i18n']['translations'] = {
export const customTranslations = {
en: {
general: {
myCustomKey: 'My custom english translation',
@@ -205,7 +207,7 @@ export const customTranslations: Config['i18n']['translations'] = {
},
}
export type CustomTranslationsObject = typeof customTranslations.en
export type CustomTranslationsObject = typeof customTranslations.en & typeof enTranslations
export type CustomTranslationsKeys = NestedKeysStripped<CustomTranslationsObject>
```
@@ -259,7 +261,10 @@ const field: Field = {
name: 'myField',
type: 'text',
label: (
{ t }: { t: TFunction<CustomTranslationsKeys | DefaultTranslationKeys> }, // The generic passed to TFunction does not automatically merge the custom translations with the default translations. We need to merge them ourselves here
) => t('fields:addLabel'),
{ t: defaultT }
) => {
const t = defaultT as TFunction<CustomTranslationsKeys>
return t('fields:addLabel')
},
}
```

View File

@@ -63,41 +63,41 @@ export default buildConfig({
The following options are available:
| Option | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
| **`debug`** | Enable to expose more detailed error information. |
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
| Option | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
| **`debug`** | Enable to expose more detailed error information. |
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
_* An asterisk denotes that a property is required._
@@ -265,3 +265,43 @@ The Payload Config can accept compatibility flags for running the newest version
Payload localization works on a field-by-field basis. As you can nest fields within other fields, you could potentially nest a localized field within a localized field—but this would be redundant and unnecessary. There would be no reason to define a localized field within a localized parent field, given that the entire data structure from the parent field onward would be localized.
By default, Payload will remove the `localized: true` property from sub-fields if a parent field is localized. Set this compatibility flag to `true` only if you have an existing Payload MongoDB database from pre-3.0, and you have nested localized fields that you would like to maintain without migrating.
## Custom bin scripts
Using the `bin` configuration property, you can inject your own scripts to `npx payload`.
Example for `pnpm payload seed`:
Step 1: create `seed.ts` file in the same folder with `payload.config.ts` with:
```ts
import type { SanitizedConfig } from 'payload'
import payload from 'payload'
// Script must define a "script" function export that accepts the sanitized config
export const script = async (config: SanitizedConfig) => {
await payload.init({ config })
await payload.create({ collection: 'pages', data: { title: 'my title' } })
payload.logger.info('Succesffully seeded!')
process.exit(0)
}
```
Step 2: add the `seed` script to `bin`:
```ts
export default buildConfig({
bin: [
{
scriptPath: path.resolve(dirname, 'seed.ts'),
key: 'seed',
},
],
})
```
Now you can run the command using:
```sh
pnpm payload seed
```

View File

@@ -119,10 +119,32 @@ For details on how to build Custom Components, see [Building Custom Components](
### Import Map
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at either `src/app/(payload)/admin/importMap.js` or `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run `payload generate:importmap` to manually regenerate it.
#### Overriding Import Map Location
Using the `config.admin.importMap.importMapFile` property, you can override the location of the import map. This is useful if you want to place the import map in a different location, or if you want to use a custom file name.
```ts
import { buildConfig } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const config = buildConfig({
// ...
admin: {
importMap: {
baseDir: path.resolve(dirname, 'src'),
importMapFile: path.resolve(dirname, 'app', '(payload)', 'custom-import-map.js'), // highlight-line
},
},
})
```
#### Custom Imports
If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location.
@@ -146,7 +168,7 @@ export default buildConfig({
},
},
}
}
})
```
## Building Custom Components
@@ -276,7 +298,7 @@ export default async function MyServerComponent({
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/hooks#useconfig) hook:
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/react-hooks#useconfig) hook:
```tsx
'use client'
@@ -375,7 +397,7 @@ export function MyClientComponent() {
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
</Banner>
### Getting the Current Locale
@@ -422,12 +444,12 @@ function Greeting() {
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
</Banner>
### Using Hooks
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/react-hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
```tsx
'use client'
@@ -444,7 +466,7 @@ export function MyClientComponent() {
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
</Banner>
### Adding Styles

View File

@@ -118,6 +118,10 @@ default, migrations will be named using a timestamp.
npm run payload migrate:create optional-name-here
```
Flags:
* `--skip-empty`: with Postgres, it skips the "no schema changes detected. Would you like to create a blank migration file?" prompt which can be useful for generating migration in CI.
* `--force-accept-warning`: accepts any command prompts, creates a blank migration even if there weren't any changes to the schema.
### Status
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run,

View File

@@ -34,11 +34,12 @@ export default buildConfig({
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
## Access to Mongoose models
@@ -50,3 +51,11 @@ You can access Mongoose models as follows:
- Collection models - `payload.db.collections[myCollectionSlug]`
- Globals model - `payload.db.globals`
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`
## Using other MongoDB implementations
Limitations with [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db):
* For Azure Cosmos DB you must pass `transactionOptions: false` to the adapter options. Azure Cosmos DB does not support transactions that update two and more documents in different collections, which is a common case when using Payload (via hooks).
* For Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
* The [Join Field](../fields/join) is not supported in DocumentDB and Azure Cosmos DB, as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future.
* For DocumentDB pass `disableIndexHints: true` to disable hinting to the DB to use `id` as index which can cause problems with DocumentDB.

View File

@@ -79,10 +79,12 @@ export const MyBlocksField: Field = {
The Blocks Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------- | ---------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
| **`isSortable`** | Disable order sorting by setting this value to `false` |
| Option | Description |
| ------------------- | -------------------------------------------------------------------------- |
| **`group`** | Text or localization object used to group this Block in the Blocks Drawer. |
| **`initCollapsed`** | Set the initial collapsed state |
| **`isSortable`** | Disable order sorting by setting this value to `false` |
| **`disableBlockName`** | Hide the blockName field by setting this value to `true` |
#### Customizing the way your block is rendered in Lexical
@@ -164,7 +166,7 @@ The `blockType` is saved as the slug of the block that has been selected.
**`blockName`**
The Admin Panel provides each block with a `blockName` field which optionally allows editors to label their blocks for better editability and readability.
The Admin Panel provides each block with a `blockName` field which optionally allows editors to label their blocks for better editability and readability. This can be visually hidden via `admin.disableBlockName`.
## Example

View File

@@ -62,6 +62,10 @@ of relationship types in an easy manner.
The Join field is extremely performant and does not add additional query overhead to your API responses until you add depth of 1 or above. It works in all database adapters. In MongoDB, we use **aggregations** to automatically join in related documents, and in relational databases, we use joins.
</Banner>
<Banner type="warning">
The Join Field is not supported in [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db), as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future.
</Banner>
### Schema advice
When modeling your database, you might come across many places where you'd like to feature bi-directional relationships.
@@ -158,6 +162,7 @@ object with:
- `docs` an array of related documents or only IDs if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
```json
{
@@ -171,7 +176,8 @@ object with:
}
// { ... }
],
"hasNextPage": false
"hasNextPage": false,
"totalDocs": 10, // if count: true is passed
}
// other fields...
}
@@ -184,6 +190,7 @@ object with:
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
```json
{
@@ -200,7 +207,8 @@ object with:
}
// { ... }
],
"hasNextPage": false
"hasNextPage": false,
"totalDocs": 10, // if count: true is passed
}
// other fields...
}
@@ -215,10 +223,11 @@ returning. This is useful for performance reasons when you don't need the relate
The following query options are supported:
| Property | Description |
|-------------|-----------------------------------------------------------------------------------------------------|
| ----------- | --------------------------------------------------------------------------------------------------- |
| **`limit`** | The maximum related documents to be returned, default is 10. |
| **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. |
| **`sort`** | A string used to order related results |
| **`count`** | Whether include the count of related documents or not. Not included by default |
These can be applied to the local API, GraphQL, and REST API.

View File

@@ -286,14 +286,15 @@ export const MyField: Field = {
The following additional properties are provided in the `ctx` object:
| Property | Description |
| Property | Description |
| --- | --- |
| `data` | An object containing the full collection or global document currently being edited. |
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
| `data` | An object containing the full collection or global document currently being edited. |
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
| `path` | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
#### Reusing Default Field Validations
@@ -502,7 +503,7 @@ export const MyCollectionConfig: CollectionConfig = {
All Description Functions receive the following arguments:
| Argument | Description |
| Argument | Description |
| --- | --- |
| **`t`** | The `t` function used to internationalize the Admin Panel. [More details](../configuration/i18n) |
@@ -512,11 +513,21 @@ All Description Functions receive the following arguments:
### Conditional Logic
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes three arguments:
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes the following arguments:
- `data` - the entire document's data that is currently being edited
- `siblingData` - only the fields that are direct siblings to the field with the condition
- `{ user }` - the final argument is an object containing the currently authenticated user
| Argument | Description |
| --- | --- |
| **`data`** | The entire document's data that is currently being edited. |
| **`siblingData`** | Only the fields that are direct siblings to the field with the condition. |
| **`ctx`** | An object containing additional information about the fields location and user. |
The `ctx` object:
| Property | Description |
| --- | --- |
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
| **`path`** | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
| **`user`** | The currently authenticated user object. |
The `condition` function should return a boolean that will control if the field should be displayed or not.
@@ -535,7 +546,7 @@ The `condition` function should return a boolean that will control if the field
type: 'text',
admin: {
// highlight-start
condition: (data, siblingData, { user }) => {
condition: (data, siblingData, { blockData, path, user }) => {
if (data.enableGreeting) {
return true
} else {
@@ -658,7 +669,7 @@ In addition to the above props, all Server Components will also receive the foll
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
To do so, import the [`useField`](../admin/hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
To do so, import the [`useField`](../admin/react-hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
```tsx
'use client'
@@ -677,7 +688,7 @@ export const CustomTextField: React.FC = () => {
```
<Banner type="success">
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/react-hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
</Banner>
##### TypeScript#field-component-types

View File

@@ -27,7 +27,7 @@ There are four main types of Hooks in Payload:
<Banner type="warning">
**Reminder:**
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/hooks).
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/react-hooks).
</Banner>
## Root Hooks

View File

@@ -44,3 +44,31 @@ const createdJob = await payload.jobs.queue({
},
})
```
#### Cancelling Jobs
Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks.
##### Cancel a Single Job
To cancel a specific job, use the `payload.jobs.cancelByID` method with the job's ID:
```ts
await payload.jobs.cancelByID({
id: createdJob.id,
})
```
##### Cancel Multiple Jobs
To cancel multiple jobs at once, use the `payload.jobs.cancel` method with a `Where` query:
```ts
await payload.jobs.cancel({
where: {
workflowSlug: {
equals: 'createPost',
},
},
})
```

View File

@@ -239,16 +239,13 @@ export const useLivePreview = <T extends any>(props: {
## Example
For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). There you will find examples of various front-end frameworks and how to integrate each one of them, including:
- [Next.js App Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-app)
- [Next.js Pages Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-pages)
For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). There you will find an example of a fully integrated Next.js App Router front-end that runs on the same server as Payload.
## Troubleshooting
#### Relationships and/or uploads are not populating
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/overview#csrf-protection) to allow cookies to be sent between the two domains. For example:
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview#cors) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/cookies#csrf-prevention) to allow cookies to be sent between the two domains. For example:
```ts
// payload.config.ts

View File

@@ -414,6 +414,15 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
```
1. The `./src/public` directory is now located directly at root level `./public` [see Next.js docs for details](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets)
1. Payload now automatically removes `localized: true` property from sub-fields if a parent is localized, as it's redunant and unnecessary. If you have some existing data in this structure and you want to disable that behavior, you need to enable `allowLocalizedWithinLocalized` flag in your payload.config [read more in documentation](https://payloadcms.com/docs/configuration/overview#compatibility-flags), or create a migration script that aligns your data.
Mongodb example for a link in a page layout.
```diff
- layout.columns.en.link.en.type.en
+ layout.columns.en.link.type
```
## Custom Components
1. All Payload React components have been moved from the `payload` package to `@payloadcms/ui`. If you were previously importing components into your app from the `payload` package, for example to create Custom Components, you will need to change your import paths:
@@ -1104,6 +1113,57 @@ plugins: [
If you have custom features for `@payloadcms/richtext-lexical` you will need to migrate your code to the new API. Read more about the new API in the [documentation](https://payloadcms.com/docs/rich-text/building-custom-features).
## Reserved Field names
Payload reserves certain field names for internal use. Using any of the following names in your collections or globals will result in those fields being sanitized from the config, which can cause deployment errors. Ensure that any conflicting fields are renamed before migrating.
### General Reserved Names
- `file`
- `_id` (MongoDB only)
- `__v` (MongoDB only)
**Important Note**: It is recommended to avoid using field names with an underscore (`_`) prefix unless explicitly required by a plugin. Payload uses this prefix for internal columns, which can lead to conflicts in certain SQL conditions. The following are examples of reserved internal columns (this list is not exhaustive and other internal fields may also apply):
- `_order`
- `_path`
- `_uuid`
- `_parent_id`
- `_locale`
### Auth-Related Reserved Names
These are restricted if your collection uses `auth: true` and does not have `disableAuthStrategy: true`:
- `salt`
- `hash`
- `apiKey` (when `auth.useAPIKey: true` is enabled)
- `useAPIKey` (when `auth.useAPIKey: true` is enabled)
- `resetPasswordToken`
- `resetPasswordExpiration`
- `password`
- `email`
- `username`
### Upload-Related Reserved Names
These apply if your collection has `upload: true` configured:
- `filename`
- `mimetype`
- `filesize`
- `width`
- `height`
- `focalX`
- `focalY`
- `url`
- `thumbnailURL`
If `imageSizes` is configured, the following are also reserved:
- `sizes`
If any of these names are found in your collection / global fields, update them before migrating to avoid unexpected issues.
## Upgrade from previous beta
Reference this [community-made site](https://payload-releases-filter.vercel.app/?version=3&from=152429656&to=188243150&sort=asc&breaking=on). Set your version, sort by oldest first, enable breaking changes only.

View File

@@ -6,7 +6,7 @@ desc: Starting to build your own plugin? Find everything you need and learn best
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Building your own [Payload Plugin](./overview) is easy, and if you&apos;re already familiar with Payload then you&apos;ll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
<Banner type="success">
To use the template, run `npx create-payload-app@latest --template plugin` directly in
@@ -19,7 +19,7 @@ Our plugin template includes everything you need to build a full life-cycle plug
- A local dev environment to develop the plugin
- Test suite with integrated GitHub workflow
By abstracting your code into a plugin, you&apos;ll be able to reuse your feature across multiple projects and make it available for other developers to use.
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
## Plugins Recap
@@ -75,7 +75,7 @@ The purpose of the **dev** folder is to provide a sanitized local Payload projec
Do **not** store any of the plugin functionality in this folder - it is purely an environment to _assist_ you with developing the plugin.
If you&apos;re starting from scratch, you can easily setup a dev environment like this:
If you're starting from scratch, you can easily setup a dev environment like this:
```
mkdir dev
@@ -83,7 +83,7 @@ cd dev
npx create-payload-app@latest
```
If you&apos;re using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
```
plugins: [
@@ -96,7 +96,7 @@ If you&apos;re using the plugin template, the dev folder is built out for you an
You can add to the `dev/payload.config.ts` and build out the dev project as needed to test your plugin.
When you&apos;re ready to start development, navigate into this folder with `cd dev`
When you're ready to start development, navigate into this folder with `cd dev`
And then start the project with `pnpm dev` and pull up `http://localhost:3000` in your browser.
@@ -108,7 +108,7 @@ A good test suite is essential to ensure quality and stability in your plugin. P
Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the [Jest documentation.](https://jestjs.io/)
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you&apos;re all set!
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set!
```
let payload: Payload
@@ -160,7 +160,7 @@ export const seed = async (payload: Payload): Promise<void> => {
## Building a Plugin
Now that we have our environment setup and dev project ready to go - it&apos;s time to build the plugin!
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
```
@@ -217,7 +217,7 @@ To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend
We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly, else this can cause adverse behavior and conflicts with Payload Config and other plugins.
Let&apos;s say you want to build a plugin that adds a new collection:
Let's say you want to build a plugin that adds a new collection:
```
config.collections = [
@@ -227,7 +227,7 @@ config.collections = [
]
```
First, you need to spread the `config.collections` to ensure that we don&apos;t lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
This same logic is applied to other array and object like properties such as admin, globals and hooks:
@@ -284,7 +284,7 @@ For a better user experience, provide a way to disable the plugin without uninst
### Include tests in your GitHub CI workflow
If you&apos;ve configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
### Publish your finished plugin to npm

View File

@@ -52,7 +52,7 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
@@ -158,6 +158,16 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
rowFields?: never
tenantFieldAccess?: never
}
/**
* Customize tenant selector label
*
* Either a string or an object where the keys are locales and the values are the string labels
*/
tenantSelectorLabel?:
| Partial<{
[key in AcceptedLanguages]?: string
}>
| string
/**
* The slug for the tenant collection
*
@@ -176,6 +186,14 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter users by selected tenant
*/
useUsersTenantFilter?: boolean
}
```
@@ -278,6 +296,50 @@ async rewrites() {
}
```
### React Hooks
Below are the hooks exported from the plugin that you can import into your own custom components to consume.
#### useTenantSelection
You can import this like so:
```tsx
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
...
const tenantContext = useTenantSelection()
```
The hook returns the following context:
```ts
type ContextType = {
/**
* Array of options to select from
*/
options: OptionObject[]
/**
* The currently selected tenant ID
*/
selectedTenantID: number | string | undefined
/**
* Prevents a refresh when the tenant is changed
*
* If not switching tenants while viewing a "global", set to true
*/
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
/**
* Sets the selected tenant ID
*
* @param args.id - The ID of the tenant to select
* @param args.refresh - Whether to refresh the page after changing the tenant
*/
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
}
```
## Examples

View File

@@ -703,6 +703,31 @@ import { addLocalesToRequestFromData } from 'payload'
}
```
`headersWithCors`
By default, custom endpoints don't handle CORS headers in responses. The `headersWithCors` function checks the Payload config and sets the appropriate CORS headers in the response accordingly.
```ts
import { headersWithCors } from 'payload'
// custom endpoint example
{
path: '/:id/tracking',
method: 'post',
handler: async (req) => {
return Response.json(
{ message: 'success' },
{
headers: headersWithCors({
headers: new Headers(),
req,
})
},
)
}
}
```
## 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.

View File

@@ -2,15 +2,15 @@
title: Lexical Converters
label: Converters
order: 20
desc: Conversion between lexical, markdown and html
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export
desc: Conversion between lexical, markdown, jsx and html
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export, jsx
---
Lexical saves data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats like JSX, HTML or Markdown.
## Lexical => JSX
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
For React-based frontends, converting Lexical content to JSX is the recommended rendering approach. Import the RichText component from @payloadcms/richtext-lexical/react and pass the Lexical content to it:
```tsx
import React from 'react'
@@ -24,46 +24,138 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
}
```
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop. In our [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) you have an example of how to use `converters` to render custom blocks, custom nodes and override existing converters.
<Banner type="default">
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.
When fetching data, ensure your `depth` setting is high enough to fully populate Lexical nodes such as uploads. The JSX converter requires fully populated data to work correctly.
</Banner>
### Converting Lexical Blocks to JSX
### Converting Internal Links
In order to convert lexical blocks or inline blocks to JSX, you will have to pass the converter for your block to the RichText component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document.
```tsx
import React from 'react'
import {
type JSXConvertersFunction,
RichText,
} from '@payloadcms/richtext-lexical/react'
import type { DefaultNodeTypes, SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
import {
type JSXConvertersFunction,
LinkJSXConverter,
RichText,
} from '@payloadcms/richtext-lexical/react'
import React from 'react'
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
const { relationTo, value } = linkNode.fields.doc!
if (typeof value !== 'object') {
throw new Error('Expected value to be an object')
}
const slug = value.slug
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
}
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
...LinkJSXConverter({ internalDocToHref }),
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Converting Lexical Blocks
To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your block to the `RichText` component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
```tsx
'use client'
import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
import React from 'react'
// Extend the default node types with your custom blocks for full type safety
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyNumberBlock | MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
// myTextBlock is the slug of the block
// Each key should match your block's slug
myNumberBlock: ({ node }) => <div>{node.fields.number}</div>,
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
},
inlineBlocks: {
// myInlineBlock is the slug of the block
// Each key should match your inline block's slug
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
},
})
export const MyComponent = ({ lexicalData }) => {
return (
<RichText
converters={jsxConverters}
data={lexicalData.lexicalWithBlocks as SerializedEditorState}
/>
)
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Overriding Default JSX Converters
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
Example - overriding the upload node converter to use next/image:
```tsx
'use client'
import type { DefaultNodeTypes, SerializedUploadNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
import Image from 'next/image'
import React from 'react'
type NodeTypes = DefaultNodeTypes
// Custom upload converter component that uses next/image
const CustomUploadComponent: React.FC<{
node: SerializedUploadNode
}> = ({ node }) => {
if (node.relationTo === 'uploads') {
const uploadDoc = node.value
if (typeof uploadDoc !== 'object') {
return null
}
const { alt, height, url, width } = uploadDoc
return <Image alt={alt} height={height} src={url} width={width} />
}
return null
}
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
// Override the default upload converter
upload: ({ node }) => {
return <CustomUploadComponent node={node} />
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
@@ -71,19 +163,156 @@ export const MyComponent = ({ lexicalData }) => {
If you don't have a React-based frontend, or if you need to send the content to a third-party service, you can convert lexical to HTML. There are two ways to do this:
1. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend.
2. **Generating HTML on any server** Convert JSON to HTML on-demand on the server.
In both cases, the conversion needs to happen on a server, as the HTML converter will automatically fetch data for nodes that require it (e.g. uploads and internal links). The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML.
1. **Generating HTML in your frontend** Convert JSON to HTML on-demand wherever you need it (Recommended).
2. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend. This is not recommended, as this approach adds additional overhead to the Payload API and may not work with live preview.
### Generating HTML in your frontend
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function exported from `@payloadcms/richtext-lexical/html`:
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
import React from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({ data })
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Generating HTML in your frontend with dynamic population
The default `convertLexicalToHTML` function does not populate data for nodes like uploads or links - it expects you to pass in the fully populated data. If you want the converter to dynamically populate those nodes as they are encountered, you have to use the async version of the converter, imported from `@payloadcms/richtext-lexical/html-async`, and pass in the `populate` function:
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const [html, setHTML] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({
apiURL: `http://localhost:3000/api`,
}),
})
setHTML(html)
}
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
Do note that using the REST populate function will result in each node sending a separate request to the REST API, which may be slow for a large amount of nodes. On the server, you can use the payload populate function, which will be more efficient:
```tsx
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import React from 'react'
import config from '../../config.js'
export const MyRSCComponent = async ({ data }: { data: SerializedEditorState }) => {
const payload = await getPayload({
config,
})
const html = await convertLexicalToHTMLAsync({
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Converting Lexical Blocks
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
})
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Outputting HTML from the Collection
To add HTML generation directly within the collection, follow the example below:
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
import type { MyTextBlock } from '@/payload-types.js'
import type { CollectionConfig } from 'payload'
import { HTMLConverterFeature, lexicalEditor, lexicalHTML } from '@payloadcms/richtext-lexical'
import {
BlocksFeature,
type DefaultNodeTypes,
lexicalEditor,
lexicalHTMLField,
type SerializedBlockNode,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
@@ -91,71 +320,53 @@ const Pages: CollectionConfig = {
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor(),
},
lexicalHTMLField({
htmlFieldName: 'nameOfYourRichTextField_html',
lexicalFieldName: 'nameOfYourRichTextField',
}),
{
name: 'customRichText',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
// The HTMLConverter Feature is the feature which manages the HTML serializers.
// If you do not pass any arguments to it, it will use the default serializers.
HTMLConverterFeature({}),
BlocksFeature({
blocks: [
{
interfaceName: 'MyTextBlock',
slug: 'myTextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
}),
],
}),
},
lexicalHTML('nameOfYourRichTextField', { name: 'nameOfYourRichTextField_html' }),
lexicalHTMLField({
htmlFieldName: 'customRichText_html',
lexicalFieldName: 'customRichText',
// can pass in additional converters or override default ones
converters: (({ defaultConverters }) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
})) as HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<MyTextBlock>>,
}),
],
}
```
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
### Generating HTML anywhere on the server
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function:
```ts
import { consolidateHTMLConverters, convertLexicalToHTML } from '@payloadcms/richtext-lexical'
await convertLexicalToHTML({
converters: consolidateHTMLConverters({ editorConfig }),
data: editorData,
payload, // if you have Payload but no req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes)
req, // if you have req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes). No need to pass in Payload if req is passed in.
})
```
This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML.
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
#### Example: Generating HTML within an afterRead hook
```ts
import type { FieldHook } from 'payload'
import {
HTMLConverterFeature,
consolidateHTMLConverters,
convertLexicalToHTML,
defaultEditorConfig,
defaultEditorFeatures,
sanitizeServerEditorConfig,
} from '@payloadcms/richtext-lexical'
const hook: FieldHook = async ({ req, siblingData }) => {
const editorConfig = defaultEditorConfig
editorConfig.features = [...defaultEditorFeatures, HTMLConverterFeature({})]
const sanitizedEditorConfig = await sanitizeServerEditorConfig(editorConfig, req.payload.config)
const html = await convertLexicalToHTML({
converters: consolidateHTMLConverters({ editorConfig: sanitizedEditorConfig }),
data: siblingData.lexicalSimple,
req,
})
return html
}
```
### CSS
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
@@ -169,141 +380,85 @@ Here is some "base" CSS you can use to ensure that nested lists render correctly
}
```
### Creating your own HTML Converter
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:
```ts
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
const UploadHTMLConverter: HTMLConverter<SerializedUploadNode> = {
converter: async ({ node, req }) => {
const uploadDocument: {
value?: any
} = {}
if(req) {
await populate({
id,
collectionSlug: node.relationTo,
currentDepth: 0,
data: uploadDocument,
depth: 1,
draft: false,
key: 'value',
overrideAccess: false,
req,
showHiddenFields: false,
})
}
const url = (req?.payload?.config?.serverURL || '') + uploadDocument?.value?.url
if (!(uploadDocument?.value?.mimeType as string)?.startsWith('image')) {
// Only images can be serialized as HTML
return ``
}
return `<img src="${url}" alt="${uploadDocument?.value?.filename}" width="${uploadDocument?.value?.width}" height="${uploadDocument?.value?.height}"/>`
},
nodeTypes: [UploadNode.getType()], // This is the type of the lexical node that this converter can handle. Instead of hardcoding 'upload' we can get the node type directly from the UploadNode, since it's static.
}
```
As you can see, we have access to all the information saved in the node (for the Upload node, this is `value`and `relationTo`) and we can use that to generate the HTML.
The `convertLexicalToHTML` is part of `@payloadcms/richtext-lexical` automatically handles traversing the editor state and calling the correct converter for each node.
### Embedding the HTML Converter in your Feature
You can embed your HTML Converter directly within your custom `ServerFeature`, allowing it to be handled automatically by the `consolidateHTMLConverters` function. Here is an example:
```ts
import { createNode } from '@payloadcms/richtext-lexical'
import type { FeatureProviderProviderServer } from '@payloadcms/richtext-lexical'
export const UploadFeature: FeatureProviderProviderServer<
UploadFeatureProps,
UploadFeaturePropsClient
> = (props) => {
/*...*/
return {
feature: () => {
return {
nodes: [
createNode({
converters: {
html: yourHTMLConverter, // <= This is where you define your HTML Converter
},
node: UploadNode,
//...
}),
],
ClientComponent: UploadFeatureClientComponent,
clientFeatureProps: clientProps,
serverFeatureProps: props,
/*...*/
}
},
key: 'upload',
serverFeatureProps: props,
}
}
```
## Headless Editor
Lexical provides a seamless way to perform conversions between various other formats:
- HTML to Lexical (or, importing HTML into the lexical editor)
- Markdown to Lexical (or, importing Markdown into the lexical editor)
- HTML to Lexical
- Markdown to Lexical
- Lexical to Markdown
A headless editor can perform such conversions outside of the main editor instance. Follow this method to initiate a headless editor:
```ts
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
import { getEnabledNodes, sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
import { getEnabledNodes, editorConfigFactory } from '@payloadcms/richtext-lexical'
const yourEditorConfig // <= your editor config here
const payloadConfig // <= your Payload Config here
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig: sanitizeServerEditorConfig(yourEditorConfig, payloadConfig),
editorConfig: await editorConfigFactory.default({config: payloadConfig})
}),
})
```
### Getting the editor config
As you can see, you need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
You need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
To get the editor config, simply import the default editor config and adjust it - just like you did inside of the `editor: lexicalEditor({})` property:
To get the editor config, import the `editorConfigFactory` factory - this factory provides a variety of ways to get the editor config, depending on your use case.
```ts
import { defaultEditorConfig, defaultEditorFeatures } from '@payloadcms/richtext-lexical' // <= make sure this package is installed
import type { SanitizedConfig } from 'payload'
const yourEditorConfig = defaultEditorConfig
import {
editorConfigFactory,
FixedToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
// If you made changes to the features of the field's editor config, you should also make those changes here:
yourEditorConfig.features = [
...defaultEditorFeatures,
// Add your custom features here
]
// Your config needs to be available in order to retrieve the default editor config
const config: SanitizedConfig = {} as SanitizedConfig
// Version 1 - use the default editor config
const yourEditorConfig = await editorConfigFactory.default({ config })
// Version 2 - if you have access to a lexical fields, you can extract the editor config from it
const yourEditorConfig2 = editorConfigFactory.fromField({
field: collectionConfig.fields[1],
})
// Version 3 - create a new editor config - behaves just like instantiating a new `lexicalEditor`
const yourEditorConfig3 = await editorConfigFactory.fromFeatures({
config,
features: ({ defaultFeatures }) => [...defaultFeatures, FixedToolbarFeature()],
})
// Version 4 - if you have instantiated a lexical editor and are accessing it outside a field (=> this is the unsanitized editor),
// you can extract the editor config from it.
// This is common if you define the editor in a re-usable module scope variable and pass it to the richText field.
// This is the least efficient way to get the editor config, and not recommended. It is recommended to extract the `features` arg
// into a separate variable and use `fromFeatures` instead.
const editor = lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures, FixedToolbarFeature()],
})
const yourEditorConfig4 = await editorConfigFactory.fromEditor({
config,
editor,
})
```
### Getting the editor config from an existing field
### Example - Getting the editor config from an existing field
If you have access to the sanitized collection config, you can get access to the lexical sanitized editor config & features, as every lexical richText field returns it. Here is an example how you can get it from another field's afterRead hook:
```ts
import type { CollectionConfig, RichTextField } from 'payload'
import { editorConfigFactory, getEnabledNodes, lexicalEditor } from '@payloadcms/richtext-lexical'
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
import type { LexicalRichTextAdapter, SanitizedServerEditorConfig } from '@payloadcms/richtext-lexical'
import {
getEnabledNodes,
lexicalEditor
} from '@payloadcms/richtext-lexical'
export const MyCollection: CollectionConfig = {
slug: 'slug',
@@ -313,20 +468,18 @@ export const MyCollection: CollectionConfig = {
type: 'text',
hooks: {
afterRead: [
({ value, collection }) => {
const otherRichTextField: RichTextField = collection.fields.find(
({ siblingFields, value }) => {
const field: RichTextField = siblingFields.find(
(field) => 'name' in field && field.name === 'richText',
) as RichTextField
const lexicalAdapter: LexicalRichTextAdapter =
otherRichTextField.editor as LexicalRichTextAdapter
const sanitizedServerEditorConfig: SanitizedServerEditorConfig =
lexicalAdapter.editorConfig
const editorConfig = editorConfigFactory.fromField({
field,
})
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig: sanitizedServerEditorConfig,
editorConfig,
}),
})
@@ -340,37 +493,43 @@ export const MyCollection: CollectionConfig = {
{
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features,
}),
}
]
editor: lexicalEditor(),
},
],
}
```
## HTML => Lexical
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
If you have access to the Payload Config and the lexical editor config, you can convert HTML to the lexical editor state with the following:
```ts
import { $generateNodesFromDOM } from '@payloadcms/richtext-lexical/lexical/html'
import { $getRoot, $getSelection } from '@payloadcms/richtext-lexical/lexical'
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
// Make sure you have jsdom and @types/jsdom installed
import { JSDOM } from 'jsdom'
const html = convertHTMLToLexical({
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),
html: '<p>text</p>',
JSDOM, // pass the JSDOM import. As it's a relatively large package, richtext-lexical does not include it by default.
})
```
## Markdown => Lexical
Convert markdown content to the Lexical editor format with the following:
```ts
import { $convertFromMarkdownString, editorConfigFactory } from '@payloadcms/richtext-lexical'
const yourEditorConfig = await editorConfigFactory.default({ config })
const markdown = `# Hello World`
headlessEditor.update(
() => {
// In a headless environment you can use a package such as JSDom to parse the HTML string.
const dom = new JSDOM(htmlString)
// Once you have the DOM instance it's easy to generate LexicalNodes.
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
// Select the root
$getRoot().select()
// Insert them at a selection.
const selection = $getSelection()
selection.insertNodes(nodes)
$convertFromMarkdownString(markdown, yourEditorConfig.features.markdownTransformers)
},
{ discrete: true },
)
@@ -390,27 +549,6 @@ This has been taken from the [lexical serialization & deserialization docs](http
immediate reading of the updated state isn't necessary, you can omit the flag.
</Banner>
## Markdown => Lexical
Convert markdown content to the Lexical editor format with the following:
```ts
import { sanitizeServerEditorConfig, $convertFromMarkdownString } from '@payloadcms/richtext-lexical'
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
const markdown = `# Hello World`
headlessEditor.update(
() => {
$convertFromMarkdownString(markdown, yourSanitizedEditorConfig.features.markdownTransformers)
},
{ discrete: true },
)
// Do this if you then want to get the editor JSON
const editorJSON = headlessEditor.getEditorState().toJSON()
```
## Lexical => Markdown
Export content from the Lexical editor into Markdown format using these steps:
@@ -421,11 +559,12 @@ Export content from the Lexical editor into Markdown format using these steps:
Here's the code for it:
```ts
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
import { editorConfigFactory } from '@payloadcms/richtext-lexical'
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
const yourEditorConfig = await editorConfigFactory.default({ config })
const yourEditorState: SerializedEditorState // <= your current editor state here
// Import editor state into your headless editor
@@ -434,7 +573,7 @@ try {
() => {
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
},
{ discrete: true }, // This should commit the editor state immediately
{ discrete: true }, // This should commit the editor state immediately
)
} catch (e) {
logger.error({ err: e }, 'ERROR parsing editor state')
@@ -443,7 +582,7 @@ try {
// Export to markdown
let markdown: string
headlessEditor.getEditorState().read(() => {
markdown = $convertToMarkdownString(yourSanitizedEditorConfig?.features?.markdownTransformers)
markdown = $convertToMarkdownString(yourEditorConfig?.features?.markdownTransformers)
})
```
@@ -464,13 +603,13 @@ const yourEditorState: SerializedEditorState // <= your current editor state her
// Import editor state into your headless editor
try {
headlessEditor.update(
headlessEditor.update(
() => {
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
},
{ discrete: true }, // This should commit the editor state immediately
{ discrete: true }, // This should commit the editor state immediately
)
} catch (e) {
} catch (e) {
logger.error({ err: e }, 'ERROR parsing editor state')
}

View File

@@ -29,9 +29,9 @@ Using the BlocksFeature, you can add both inline blocks (= can be inserted into
### Example: Code Field Block with language picker
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. Make sure to manually install `@payloadcms/ui`first.
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. First, make sure to explicitly install `@payloadcms/ui` in your project.
Field config:
Field Config:
```ts
import {
@@ -91,7 +91,6 @@ CodeComponent.tsx:
```tsx
'use client'
import type { CodeFieldClient, CodeFieldClientProps } from 'payload'
import { CodeField, useFormFields } from '@payloadcms/ui'
@@ -105,6 +104,8 @@ const languageKeyToMonacoLanguageMap = {
tsx: 'typescript',
}
type Language = keyof typeof languageKeyToMonacoLanguageMap
export const Code: React.FC<CodeFieldClientProps> = ({
autoComplete,
field,
@@ -118,10 +119,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
}) => {
const languageField = useFormFields(([fields]) => fields['language'])
const language: string =
(languageField?.value as string) || (languageField.initialValue as string) || 'typescript'
const language: Language =
(languageField?.value as Language) || (languageField?.initialValue as Language) || 'ts'
const label = languages[language as keyof typeof languages]
const label = languages[language]
const props: CodeFieldClient = useMemo<CodeFieldClient>(
() => ({
@@ -129,9 +130,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
type: 'code',
admin: {
...field.admin,
label,
editorOptions: undefined,
language: languageKeyToMonacoLanguageMap[language] || language,
},
label,
}),
[field, language, label],
)

View File

@@ -145,20 +145,20 @@ Here's an overview of all the included features:
| Feature Name | Included by default | Description |
| --- | --- | --- |
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
| **`BoldFeature`** | Yes | Handles the bold text format |
| **`ItalicFeature`** | Yes | Handles the italic text format |
| **`UnderlineFeature`** | Yes | Handles the underline text format |
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
| **`CheckListFeature`** | Yes | Adds checklists |
| **`ChecklistFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |

View File

@@ -185,6 +185,8 @@ The [Admin Panel](../admin/overview) will also automatically display all availab
Behind the scenes, Payload relies on [`sharp`](https://sharp.pixelplumbing.com/api-resize#resize) to perform its image resizing. You can specify additional options for `sharp` to use while resizing your images.
Note that for image resizing to work, `sharp` must be specified in your [Payload Config](../configuration/overview). This is configured by default if you created your Payload project with `create-payload-app`. See `sharp` in [Config Options](../configuration/overview#config-options).
#### Accessing the resized images in hooks
All auto-resized images are exposed to be re-used in hooks and similar via an object that is bound to `req.payloadUploadSizes`.
@@ -347,6 +349,32 @@ fetch('api/:upload-slug', {
})
```
## Uploading Files stored locally
If you want to upload a file stored on your machine directly using the `payload.create` method, for example, during a seed script,
you can use the `filePath` property to specify the local path of the file.
```ts
const localFilePath = path.resolve(__dirname, filename)
await payload.create({
collection: 'media',
data: {
alt,
},
filePath: localFilePath,
})
```
The `data` property should still include all the required fields of your `media` collection.
<Banner type="warning">
**Important:**
Remember that all custom hooks attached to the `media` collection will still trigger.
Ensure that files match the specified mimeTypes or sizes defined in the collection's `formatOptions` or custom `hooks`.
</Banner>
## Uploading Files from Remote URLs
The `pasteURL` option allows users to fetch files from remote URLs by pasting them into an Upload field. This option is **enabled by default** and can be configured to either **allow unrestricted client-side fetching** or **restrict server-side fetching** to specific trusted domains.

View File

@@ -30,6 +30,7 @@ pnpm add @payloadcms/storage-vercel-blob
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
- Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
```ts
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
@@ -64,6 +65,7 @@ export default buildConfig({
| `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` |
| `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) |
| `token` | Vercel Blob storage read/write token | `''` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## S3 Storage
[`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3)
@@ -79,6 +81,7 @@ pnpm add @payloadcms/storage-s3
- Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs.
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
```ts
import { s3Storage } from '@payloadcms/storage-s3'
@@ -126,6 +129,7 @@ pnpm add @payloadcms/storage-azure
- Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website.
```ts
import { azureStorage } from '@payloadcms/storage-azure'
@@ -161,6 +165,7 @@ export default buildConfig({
| `baseURL` | Base URL for the Azure Blob storage account | |
| `connectionString` | Azure Blob storage connection string | |
| `containerName` | Azure Blob storage container name | |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## Google Cloud Storage
[`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs)
@@ -175,6 +180,7 @@ pnpm add @payloadcms/storage-gcs
- Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
```ts
import { gcsStorage } from '@payloadcms/storage-gcs'
@@ -203,13 +209,14 @@ export default buildConfig({
### Configuration Options#gcs-configuration
| Option | Description | Default |
| ------------- | --------------------------------------------------------------------------------------------------- | --------- |
| `enabled` | Whether or not to enable the plugin | `true` |
| `collections` | Collections to apply the storage to | |
| `bucket` | The name of the bucket to use | |
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
| `acl` | Access control list for files that are uploaded | `Private` |
| Option | Description | Default |
| --------------- | --------------------------------------------------------------------------------------------------- | --------- |
| `enabled` | Whether or not to enable the plugin | `true` |
| `collections` | Collections to apply the storage to | |
| `bucket` | The name of the bucket to use | |
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
| `acl` | Access control list for files that are uploaded | `Private` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## Uploadthing Storage
@@ -226,6 +233,7 @@ pnpm add @payloadcms/storage-uploadthing
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
- Get a token from Uploadthing and set it as `token` in the `options` object.
- `acl` is optional and defaults to `public-read`.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
```ts
export default buildConfig({
@@ -246,13 +254,14 @@ export default buildConfig({
### Configuration Options#uploadthing-configuration
| Option | Description | Default |
| ---------------- | ----------------------------------------------- | ------------- |
| `token` | Token from Uploadthing. Required. | |
| `acl` | Access control list for files that are uploaded | `public-read` |
| `logLevel` | Log level for Uploadthing | `info` |
| `fetch` | Custom fetch function | `fetch` |
| `defaultKeyType` | Default key type for file operations | `fileKey` |
| Option | Description | Default |
| ---------------- | ------------------------------------------------------------- | ------------- |
| `token` | Token from Uploadthing. Required. | |
| `acl` | Access control list for files that are uploaded | `public-read` |
| `logLevel` | Log level for Uploadthing | `info` |
| `fetch` | Custom fetch function | `fetch` |
| `defaultKeyType` | Default key type for file operations | `fileKey` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## Custom Storage Adapters

View File

@@ -68,7 +68,7 @@ If your front-end is statically generated then you may also want to regenerate t
### Admin Bar
You might also want to render an admin bar on your front-end so that logged-in users can quickly navigate between the front-end and Payload as they're editing. For React apps, check out the official [Payload Admin Bar](https://github.com/payloadcms/payload-admin-bar). For other frameworks, simply hit the `/me` route with `credentials: 'include'` and render your own admin bar if the user is logged in.
You might also want to render an admin bar on your front-end so that logged-in users can quickly navigate between the front-end and Payload as they're editing. For React apps, check out the official [Payload Admin Bar](https://github.com/payloadcms/payload/tree/main/packages/admin-bar). For other frameworks, simply hit the `/me` route with `credentials: 'include'` and render your own admin bar if the user is logged in.
### CORS

View File

@@ -16,6 +16,7 @@
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/admin-bar": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/next": "latest",
"@payloadcms/richtext-slate": "latest",
@@ -25,7 +26,6 @@
"graphql": "^16.9.0",
"next": "^15.0.0",
"payload": "latest",
"payload-admin-bar": "^1.0.6",
"react": "19.0.0",
"react-dom": "19.0.0"
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
'use client'
import type { PayloadAdminBarProps } from 'payload-admin-bar'
import type { PayloadAdminBarProps } from '@payloadcms/admin-bar'
import { useRouter } from 'next/navigation'
import { PayloadAdminBar } from 'payload-admin-bar'
import { PayloadAdminBar } from '@payloadcms/admin-bar'
import React, { useState } from 'react'
import { Gutter } from '../Gutter'
@@ -42,7 +42,7 @@ export const AdminBar: React.FC<{
user: classes.user,
}}
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
collection={collection}
collectionSlug={collection}
collectionLabels={{
plural: collectionLabels[collection]?.plural || 'Pages',
singular: collectionLabels[collection]?.singular || 'Page',

View File

@@ -52,7 +52,7 @@ export const home: Page = {
children: [{ text: 'Payload Admin Bar' }],
linkType: 'custom',
newTab: true,
url: 'https://github.com/payloadcms/payload-admin-bar',
url: 'https://github.com/payloadcms/payload/tree/main/packages/admin-bar',
},
{
text: ' appear at the top of this site. This will allow you to seamlessly navigate between the two apps. Then, navigate to the ',

View File

@@ -42,7 +42,6 @@
"next": "^15.1.0",
"next-intl": "^3.23.2",
"payload": "latest",
"payload-admin-bar": "^1.0.6",
"prism-react-renderer": "^2.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import { GeistMono } from 'geist/font/mono'
import { GeistSans } from 'geist/font/sans'
import React from 'react'
import { AdminBar } from '@/components/AdminBar'
import { Footer } from '@/globals/Footer/Component'
import { Header } from '@/globals/Header/Component'
import { LivePreviewListener } from '@/components/LivePreviewListener'
@@ -57,13 +56,7 @@ export default async function RootLayout({ children, params }: Args) {
<body>
<Providers>
<NextIntlClientProvider messages={messages}>
<AdminBar
adminBarProps={{
preview: isEnabled,
}}
/>
<LivePreviewListener />
<Header locale={locale} />
{children}
<Footer locale={locale} />

View File

@@ -1,7 +0,0 @@
@import '~@payloadcms/ui/scss';
.admin-bar {
@include small-break {
display: none;
}
}

View File

@@ -1,85 +0,0 @@
'use client'
import type { PayloadAdminBarProps } from 'payload-admin-bar'
import { cn } from '@/utilities/ui'
import { useSelectedLayoutSegments } from 'next/navigation'
import { PayloadAdminBar } from 'payload-admin-bar'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import './index.scss'
import { useTranslations } from 'next-intl'
const baseClass = 'admin-bar'
const collectionLabels = {
pages: {
plural: 'Pages',
singular: 'Page',
},
posts: {
plural: 'Posts',
singular: 'Post',
},
projects: {
plural: 'Projects',
singular: 'Project',
},
}
export const AdminBar: React.FC<{
adminBarProps?: PayloadAdminBarProps
}> = (props) => {
const { adminBarProps } = props || {}
const segments = useSelectedLayoutSegments()
const [show, setShow] = useState(false)
const collection = collectionLabels?.[segments?.[1]] ? segments?.[1] : 'pages'
const router = useRouter()
const t = useTranslations()
const onAuthChange = React.useCallback((user) => {
setShow(user?.id)
}, [])
return (
<div
className={cn(baseClass, 'py-2 bg-black text-white', {
block: show,
hidden: !show,
})}
>
<div className="container">
<PayloadAdminBar
{...adminBarProps}
className="py-2 text-white"
classNames={{
controls: 'font-medium text-white',
logo: 'text-white',
user: 'text-white',
}}
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
collection={collection}
collectionLabels={{
plural: collectionLabels[collection]?.plural || 'Pages',
singular: collectionLabels[collection]?.singular || 'Page',
}}
logo={<span>{t('dashboard')}</span>}
onAuthChange={onAuthChange}
onPreviewExit={() => {
fetch('/next/exit-preview').then(() => {
router.push('/')
router.refresh()
})
}}
style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative',
zIndex: 'unset',
}}
/>
</div>
</div>
)
}

View File

@@ -1,3 +1,5 @@
DATABASE_URI=mongodb://127.0.0.1/payload-example-multi-tenant
POSTGRES_URL=postgres://127.0.0.1:5432/payload-example-multi-tenant
PAYLOAD_SECRET=PAYLOAD_MULTI_TENANT_EXAMPLE_SECRET_KEY
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
SEED_DB=true

View File

@@ -46,12 +46,12 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
**Domain-based Tenant Setting**:
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.test:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.localhost:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
For the domain portion of the example to function properly, you will need to add the following entries to your system's `/etc/hosts` file:
```
127.0.0.1 gold.test silver.test bronze.test
127.0.0.1 gold.localhost silver.localhost bronze.localhost
```
- #### Pages

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -17,6 +17,7 @@
},
"dependencies": {
"@payloadcms/db-mongodb": "latest",
"@payloadcms/db-postgres": "^3.25.0",
"@payloadcms/next": "latest",
"@payloadcms/plugin-multi-tenant": "latest",
"@payloadcms/richtext-lexical": "latest",

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,10 @@ export default async ({ params: paramsPromise }: { params: Promise<{ slug: strin
<p>When you visit a tenant by domain, the domain is used to determine the tenant.</p>
<p>
For example, visiting{' '}
<a href="http://gold.test:3000/tenant-domains/login">
http://gold.test:3000/tenant-domains/login
<a href="http://gold.localhost:3000/tenant-domains/login">
http://gold.localhost:3000/tenant-domains/login
</a>{' '}
will show the tenant with the domain "gold.test".
will show the tenant with the domain "gold.localhost".
</p>
<h2>Slugs</h2>

View File

@@ -1,12 +1,9 @@
import { TenantField as TenantField_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
import { TenantSelectionProvider as TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
import { TenantSelector as TenantSelector_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
export const importMap = {
'@payloadcms/plugin-multi-tenant/client#TenantField':
TenantField_1d0591e3cf4f332c83a86da13a0de59a,
'@payloadcms/plugin-multi-tenant/rsc#TenantSelector':
TenantSelector_d6d5f193a167989e2ee7d14202901e62,
'@payloadcms/plugin-multi-tenant/client#TenantSelectionProvider':
TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a,
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
"@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a,
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
}

View File

@@ -1,8 +1,9 @@
import type { FieldHook } from 'payload'
import type { FieldHook, Where } from 'payload'
import { ValidationError } from 'payload'
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
import { extractID } from '@/utilities/extractID'
export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation
@@ -10,26 +11,30 @@ export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, valu
return value
}
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
const currentTenantID =
typeof originalDoc?.tenant === 'object' ? originalDoc.tenant.id : originalDoc?.tenant
const constraints: Where[] = [
{
slug: {
equals: value,
},
},
]
const incomingTenantID = extractID(data?.tenant)
const currentTenantID = extractID(originalDoc?.tenant)
const tenantIDToMatch = incomingTenantID || currentTenantID
if (tenantIDToMatch) {
constraints.push({
tenant: {
equals: tenantIDToMatch,
},
})
}
const findDuplicatePages = await req.payload.find({
collection: 'pages',
where: {
and: [
{
tenant: {
equals: tenantIDToMatch,
},
},
{
slug: {
equals: value,
},
},
],
and: constraints,
},
})

View File

@@ -1,11 +1,11 @@
import type { User } from '@/payload-types'
import type { Access, Where } from 'payload'
import { parseCookies } from 'payload'
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
import { isAccessingSelf } from './isAccessingSelf'
import { getCollectionIDType } from '@/utilities/getCollectionIDType'
export const readAccess: Access<User> = ({ req, id }) => {
if (!req?.user) {
@@ -16,9 +16,11 @@ export const readAccess: Access<User> = ({ req, id }) => {
return true
}
const cookies = parseCookies(req.headers)
const superAdmin = isSuperAdmin(req.user)
const selectedTenant = cookies.get('payload-tenant')
const selectedTenant = getTenantFromCookie(
req.headers,
getCollectionIDType({ payload: req.payload, collectionSlug: 'tenants' }),
)
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
if (selectedTenant) {

View File

@@ -1,8 +1,11 @@
import type { FieldHook } from 'payload'
import type { FieldHook, Where } from 'payload'
import { ValidationError } from 'payload'
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
import { extractID } from '@/utilities/extractID'
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
import { getCollectionIDType } from '@/utilities/getCollectionIDType'
export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation
@@ -10,26 +13,31 @@ export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req,
return value
}
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
const currentTenantID =
typeof originalDoc?.tenant === 'object' ? originalDoc.tenant.id : originalDoc?.tenant
const tenantIDToMatch = incomingTenantID || currentTenantID
const constraints: Where[] = [
{
username: {
equals: value,
},
},
]
const selectedTenant = getTenantFromCookie(
req.headers,
getCollectionIDType({ payload: req.payload, collectionSlug: 'tenants' }),
)
if (selectedTenant) {
constraints.push({
'tenants.tenant': {
equals: selectedTenant,
},
})
}
const findDuplicateUsers = await req.payload.find({
collection: 'users',
where: {
and: [
{
'tenants.tenant': {
equals: tenantIDToMatch,
},
},
{
username: {
equals: value,
},
},
],
and: constraints,
},
})
@@ -39,7 +47,8 @@ export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req,
// provide a more specific error message
if (req.user.roles?.includes('super-admin') || tenantIDs.length > 1) {
const attemptedTenantChange = await req.payload.findByID({
id: tenantIDToMatch,
// @ts-ignore - selectedTenant will match DB ID type
id: selectedTenant,
collection: 'tenants',
})

View File

@@ -1,7 +1,6 @@
import type { CollectionAfterLoginHook } from 'payload'
import { mergeHeaders } from '@payloadcms/next/utilities'
import { generateCookie, getCookieExpiration } from 'payload'
import { mergeHeaders, generateCookie, getCookieExpiration } from 'payload'
export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, user }) => {
const relatedOrg = await req.payload.find({
@@ -22,7 +21,7 @@ export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, us
expires: getCookieExpiration({ seconds: 7200 }),
path: '/',
returnCookieAsObject: false,
value: relatedOrg.docs[0].id,
value: String(relatedOrg.docs[0].id),
})
// Merge existing responseHeaders with the new Set-Cookie header

View File

@@ -6,10 +6,65 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
pages: Page;
users: User;
@@ -28,7 +83,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
defaultIDType: number;
};
globals: {};
globalsSelect: {};
@@ -64,8 +119,8 @@ export interface UserAuthOperations {
* via the `definition` "pages".
*/
export interface Page {
id: string;
tenant?: (string | null) | Tenant;
id: number;
tenant?: (number | null) | Tenant;
title?: string | null;
slug?: string | null;
updatedAt: string;
@@ -76,7 +131,7 @@ export interface Page {
* via the `definition` "tenants".
*/
export interface Tenant {
id: string;
id: number;
name: string;
/**
* Used for domain-based tenant handling
@@ -98,12 +153,12 @@ export interface Tenant {
* via the `definition` "users".
*/
export interface User {
id: string;
id: number;
roles?: ('super-admin' | 'user')[] | null;
username?: string | null;
tenants?:
| {
tenant: string | Tenant;
tenant: number | Tenant;
roles: ('tenant-admin' | 'tenant-viewer')[];
id?: string | null;
}[]
@@ -124,24 +179,24 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
id: number;
document?:
| ({
relationTo: 'pages';
value: string | Page;
value: number | Page;
} | null)
| ({
relationTo: 'users';
value: string | User;
value: number | User;
} | null)
| ({
relationTo: 'tenants';
value: string | Tenant;
value: number | Tenant;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
updatedAt: string;
createdAt: string;
@@ -151,10 +206,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
id: number;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
key?: string | null;
value?:
@@ -174,7 +229,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;

View File

@@ -1,4 +1,5 @@
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
@@ -11,6 +12,7 @@ import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
import { isSuperAdmin } from './access/isSuperAdmin'
import type { Config } from './payload-types'
import { getUserTenantIDs } from './utilities/getUserTenantIDs'
import { seed } from './seed'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -21,9 +23,19 @@ export default buildConfig({
user: 'users',
},
collections: [Pages, Users, Tenants],
db: mongooseAdapter({
url: process.env.DATABASE_URI as string,
// db: mongooseAdapter({
// url: process.env.DATABASE_URI as string,
// }),
db: postgresAdapter({
pool: {
connectionString: process.env.POSTGRES_URL,
},
}),
onInit: async (args) => {
if (process.env.SEED_DB) {
await seed(args)
}
},
editor: lexicalEditor({}),
graphQL: {
schemaOutputFile: path.resolve(dirname, 'generated-schema.graphql'),

View File

@@ -1,12 +1,12 @@
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
import { Config } from 'payload'
export async function up({ payload }: MigrateUpArgs): Promise<void> {
export const seed: NonNullable<Config['onInit']> = async (payload): Promise<void> => {
const tenant1 = await payload.create({
collection: 'tenants',
data: {
name: 'Tenant 1',
slug: 'gold',
domain: 'gold.test',
domain: 'gold.localhost',
},
})
@@ -15,7 +15,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
data: {
name: 'Tenant 2',
slug: 'silver',
domain: 'silver.test',
domain: 'silver.localhost',
},
})
@@ -24,7 +24,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
data: {
name: 'Tenant 3',
slug: 'bronze',
domain: 'bronze.test',
domain: 'bronze.localhost',
},
})

View File

@@ -0,0 +1,9 @@
import type { CollectionSlug, Payload } from 'payload'
type Args = {
collectionSlug: CollectionSlug
payload: Payload
}
export const getCollectionIDType = ({ collectionSlug, payload }: Args): 'number' | 'text' => {
return payload.collections[collectionSlug]?.customIDType ?? payload.db.defaultIDType
}

View File

@@ -12,53 +12,57 @@ const withBundleAnalyzer = bundleAnalyzer({
})
const config = withBundleAnalyzer(
withPayload({
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
experimental: {
serverActions: {
bodySizeLimit: '5mb',
withPayload(
{
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
experimental: {
fullySpecified: true,
serverActions: {
bodySizeLimit: '5mb',
},
},
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),
// @todo remove in 4.0 - will behave like this by default in 4.0
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
},
async redirects() {
return [
{
destination: '/admin',
permanent: false,
source: '/',
},
]
},
images: {
domains: ['localhost'],
},
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'],
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
}
// Ignore sentry warnings when not wrapped with withSentryConfig
webpackConfig.ignoreWarnings = [
...(webpackConfig.ignoreWarnings ?? []),
{ file: /esm\/platform\/node\/instrumentation.js/ },
{ module: /esm\/platform\/node\/instrumentation.js/ },
]
return webpackConfig
},
},
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),
// @todo remove in 4.0 - will behave like this by default in 4.0
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
},
async redirects() {
return [
{
destination: '/admin',
permanent: false,
source: '/',
},
]
},
images: {
domains: ['localhost'],
},
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'],
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
}
// Ignore sentry warnings when not wrapped with withSentryConfig
webpackConfig.ignoreWarnings = [
...(webpackConfig.ignoreWarnings ?? []),
{ file: /esm\/platform\/node\/instrumentation.js/ },
{ module: /esm\/platform\/node\/instrumentation.js/ },
]
return webpackConfig
},
}),
{ devBundleServerPackages: false },
),
)
export default process.env.NEXT_PUBLIC_SENTRY_DSN

View File

@@ -1,11 +1,12 @@
{
"name": "payload-monorepo",
"version": "3.24.0",
"version": "3.28.0",
"private": true,
"type": "module",
"scripts": {
"bf": "pnpm run build:force",
"build": "pnpm run build:core",
"build:admin-bar": "turbo build --filter \"@payloadcms/admin-bar\"",
"build:all": "turbo build",
"build:app": "next build",
"build:app:analyze": "cross-env ANALYZE=true next build",
@@ -33,6 +34,7 @@
"build:payload-cloud": "turbo build --filter \"@payloadcms/payload-cloud\"",
"build:plugin-cloud-storage": "turbo build --filter \"@payloadcms/plugin-cloud-storage\"",
"build:plugin-form-builder": "turbo build --filter \"@payloadcms/plugin-form-builder\"",
"build:plugin-import-export": "turbo build --filter \"@payloadcms/plugin-import-export\"",
"build:plugin-multi-tenant": "turbo build --filter \"@payloadcms/plugin-multi-tenant\"",
"build:plugin-nested-docs": "turbo build --filter \"@payloadcms/plugin-nested-docs\"",
"build:plugin-redirects": "turbo build --filter \"@payloadcms/plugin-redirects\"",
@@ -79,10 +81,9 @@
"prepare": "husky",
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
"reinstall": "pnpm clean:all && pnpm install",
"release": "pnpm --filter releaser release --tag latest",
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
"release:canary": "pnpm --filter releaser release:canary",
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
"script:gen-templates": "pnpm --filter scripts gen-templates",
@@ -117,7 +118,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.1.5",
"@next/bundle-analyzer": "15.2.2",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -132,8 +133,8 @@
"@types/jest": "29.5.12",
"@types/minimist": "1.2.5",
"@types/node": "22.5.4",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/shelljs": "0.8.15",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -153,7 +154,7 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^10",
"next": "15.1.5",
"next": "15.2.2",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.50.0",
@@ -173,10 +174,6 @@
"turbo": "^2.3.3",
"typescript": "5.7.3"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
},
"packageManager": "pnpm@9.7.1",
"engines": {
"node": "^18.20.2 || >=20.9.0",

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

24
packages/admin-bar/.swcrc Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {
"type": "es6"
}
}

View File

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

View File

@@ -0,0 +1,119 @@
# Payload Admin Bar
An admin bar for React apps using [Payload](https://github.com/payloadcms/payload).
### Installation
```bash
pnpm i @payloadcms/admin-bar
```
### Basic Usage
```jsx
import { PayloadAdminBar } from '@payloadcms/admin-bar'
export const App = () => {
return <PayloadAdminBar cmsURL="https://cms.website.com" collection="pages" id="12345" />
}
```
Checks for authentication with Payload CMS by hitting the [`/me`](https://payloadcms.com/docs/authentication/operations#me) route. If authenticated, renders an admin bar with simple controls to do the following:
- Navigate to the admin dashboard
- Navigate to the currently logged-in user's account
- Edit the current collection
- Create a new collection of the same type
- Logout
- Indicate and exit preview mode
The admin bar ships with very little style and is fully customizable.
### Dynamic props
With client-side routing, we need to update the admin bar with a new collection type and document id on each route change. This will depend on your app's specific setup, but here are a some common examples:
#### NextJS
For NextJS apps using dynamic-routes, use `getStaticProps`:
```ts
export const getStaticProps = async ({ params: { slug } }) => {
const props = {}
const pageReq = await fetch(
`https://cms.website.com/api/pages?where[slug][equals]=${slug}&depth=1`,
)
const pageData = await pageReq.json()
if (pageReq.ok) {
const { docs } = pageData
const [doc] = docs
props = {
...doc,
collection: 'pages',
collectionLabels: {
singular: 'page',
plural: 'pages',
},
}
}
return props
}
```
Now your app can forward these props onto the admin bar. Something like this:
```ts
import { PayloadAdminBar } from '@payloadcms/admin-bar';
export const App = (appProps) => {
const {
pageProps: {
collection,
collectionLabels,
id
}
} = appProps;
return (
<PayloadAdminBar
{...{
cmsURL: 'https://cms.website.com',
collection,
collectionLabels,
id
}}
/>
)
}
```
### Props
| Property | Type | Required | Default | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cmsURL | `string` | true | `http://localhost:8000` | `serverURL` as defined in your [Payload config](https://payloadcms.com/docs/configuration/overview#options) |
| adminPath | `string` | false | /admin | `routes` as defined in your [Payload config](https://payloadcms.com/docs/configuration/overview#options) |
| apiPath | `string` | false | /api | `routes` as defined in your [Payload config](https://payloadcms.com/docs/configuration/overview#options) |
| authCollectionSlug | `string` | false | 'users' | Slug of your [auth collection](https://payloadcms.com/docs/configuration/collections) |
| collectionSlug | `string` | true | undefined | Slug of your [collection](https://payloadcms.com/docs/configuration/collections) |
| collectionLabels | `{ singular?: string, plural?: string }` | false | undefined | Labels of your [collection](https://payloadcms.com/docs/configuration/collections) |
| id | `string` | true | undefined | id of the document |
| logo | `ReactElement` | false | undefined | Custom logo |
| classNames | `{ logo?: string, user?: string, controls?: string, create?: string, logout?: string, edit?: string, preview?: string }` | false | undefined | Custom class names, one for each rendered element |
| logoProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
| userProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
| divProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
| createProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
| logoutProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
| editProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
| previewProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
| style | `CSSProperties` | false | undefined | Custom inline style |
| unstyled | `boolean` | false | undefined | If true, renders no inline style |
| onAuthChange | `(user: PayloadMeUser) => void` | false | undefined | Fired on each auth change |
| devMode | `boolean` | false | undefined | If true, fakes authentication (useful when dealing with [SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)) |
| preview | `boolean` | false | undefined | If true, renders an exit button with your `onPreviewExit` handler) |
| onPreviewExit | `function` | false | undefined | Callback for the preview button `onClick` event) |

View File

@@ -0,0 +1,18 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

View File

@@ -0,0 +1,65 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.28.0",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/admin-bar"
},
"license": "MIT",
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
"maintainers": [
{
"name": "Payload",
"email": "info@payloadcms.com",
"url": "https://payloadcms.com"
}
],
"type": "module",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf -g {dist,*.tsbuildinfo}",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"payload": "workspace:*"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
}
}

View File

@@ -0,0 +1,349 @@
'use client'
import React, { useEffect, useState } from 'react'
const dummyUser = {
id: '12345',
email: 'dev@email.com',
}
import type { PayloadAdminBarProps, PayloadMeUser } from './types.js'
export const PayloadAdminBar: React.FC<PayloadAdminBarProps> = (props) => {
const {
id: docID,
adminPath = '/admin',
apiPath = '/api',
authCollectionSlug = 'users',
className,
classNames,
cmsURL = 'http://localhost:8000',
collectionLabels,
collectionSlug,
createProps,
devMode,
divProps,
editProps,
logo,
logoProps,
logoutProps,
onAuthChange,
onPreviewExit,
preview,
previewProps,
style,
unstyled,
userProps,
} = props
const [user, setUser] = useState<PayloadMeUser>()
useEffect(() => {
const fetchMe = async () => {
try {
const meRequest = await fetch(`${cmsURL}${apiPath}/${authCollectionSlug}/me`, {
credentials: 'include',
method: 'get',
})
const meResponse = await meRequest.json()
const { user } = meResponse
if (user) {
setUser(user)
} else {
if (devMode !== true) {
setUser(null)
} else {
setUser(dummyUser)
}
}
} catch (err) {
console.warn(err)
if (devMode === true) {
setUser(dummyUser)
}
}
}
void fetchMe()
}, [cmsURL, adminPath, apiPath, devMode])
useEffect(() => {
if (typeof onAuthChange === 'function') {
onAuthChange(user)
}
}, [user, onAuthChange])
if (user) {
const { id: userID, email } = user
return (
<div
className={className}
id="payload-admin-bar"
style={{
...(unstyled !== true
? {
alignItems: 'center',
backgroundColor: '#222',
boxSizing: 'border-box',
color: '#fff',
display: 'flex',
fontFamily:
'-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif',
fontSize: 'small',
left: 0,
minWidth: '0',
padding: '0.5rem',
position: 'fixed',
top: 0,
width: '100%',
zIndex: 99999,
}
: {}),
...style,
}}
>
<a
className={classNames?.logo}
href={`${cmsURL}${adminPath}`}
{...logoProps}
style={{
...(unstyled !== true
? {
alignItems: 'center',
color: 'inherit',
display: 'flex',
flexShrink: 0,
height: '20px',
marginRight: '10px',
textDecoration: 'none',
...(logoProps?.style
? {
...logoProps.style,
}
: {}),
}
: {}),
}}
>
{logo || 'Payload CMS'}
</a>
<a
className={classNames?.user}
href={`${cmsURL}${adminPath}/collections/${authCollectionSlug}/${userID}`}
rel="noopener noreferrer"
target="_blank"
{...userProps}
style={{
...(unstyled !== true
? {
color: 'inherit',
display: 'block',
marginRight: '10px',
minWidth: '50px',
overflow: 'hidden',
textDecoration: 'none',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
...(userProps?.style
? {
...userProps.style,
}
: {}),
}
: {}),
}}
>
<span
style={{
...(unstyled !== true
? {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}
: {}),
}}
>
{email || 'Profile'}
</span>
</a>
<div
className={classNames?.controls}
{...divProps}
style={{
...(unstyled !== true
? {
alignItems: 'center',
display: 'flex',
flexGrow: 1,
flexShrink: 1,
justifyContent: 'flex-end',
marginRight: '10px',
...(divProps?.style
? {
...divProps.style,
}
: {}),
}
: {}),
}}
>
{collectionSlug && docID && (
<a
className={classNames?.edit}
href={`${cmsURL}${adminPath}/collections/${collectionSlug}/${docID}`}
rel="noopener noreferrer"
target="_blank"
{...editProps}
style={{
display: 'block',
...(unstyled !== true
? {
color: 'inherit',
flexShrink: 1,
marginRight: '10px',
overflow: 'hidden',
textDecoration: 'none',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
...(editProps?.style
? {
...editProps.style,
}
: {}),
}
: {}),
}}
>
<span
style={{
...(unstyled !== true
? {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}
: {}),
}}
>
{`Edit ${collectionLabels?.singular || 'page'}`}
</span>
</a>
)}
{collectionSlug && (
<a
className={classNames?.create}
href={`${cmsURL}${adminPath}/collections/${collectionSlug}/create`}
rel="noopener noreferrer"
target="_blank"
{...createProps}
style={{
...(unstyled !== true
? {
color: 'inherit',
display: 'block',
flexShrink: 1,
overflow: 'hidden',
textDecoration: 'none',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
...(createProps?.style
? {
...createProps.style,
}
: {}),
}
: {}),
}}
>
<span
style={{
...(unstyled !== true
? {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}
: {}),
}}
>
{`New ${collectionLabels?.singular || 'page'}`}
</span>
</a>
)}
{preview && (
<button
className={classNames?.preview}
onClick={onPreviewExit}
{...previewProps}
style={{
...(unstyled !== true
? {
background: 'none',
border: 'none',
color: 'inherit',
cursor: 'pointer',
fontFamily: 'inherit',
fontSize: 'inherit',
marginLeft: '10px',
padding: 0,
...(previewProps?.style
? {
...previewProps.style,
}
: {}),
}
: {}),
}}
type="button"
>
Exit preview mode
</button>
)}
</div>
<a
className={classNames?.logout}
href={`${cmsURL}${adminPath}/logout`}
rel="noopener noreferrer"
target="_blank"
{...logoutProps}
style={{
...(unstyled !== true
? {
color: 'inherit',
display: 'block',
flexShrink: 1,
overflow: 'hidden',
textDecoration: 'none',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
...(logoutProps?.style
? {
...logoutProps.style,
}
: {}),
}
: {}),
}}
>
<span
style={{
...(unstyled !== true
? {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}
: {}),
}}
>
Logout
</span>
</a>
</div>
)
}
return null
}

View File

@@ -0,0 +1,2 @@
export { PayloadAdminBar } from './AdminBar.js'
export type { PayloadAdminBarProps, PayloadMeUser } from './types.js'

View File

@@ -0,0 +1,67 @@
import type { CSSProperties, ReactElement } from 'react'
export type PayloadMeUser =
| {
email: string
id: string
}
| null
| undefined
export type PayloadAdminBarProps = {
adminPath?: string
apiPath?: string
authCollectionSlug?: string
className?: string
classNames?: {
controls?: string
create?: string
edit?: string
logo?: string
logout?: string
preview?: string
user?: string
}
cmsURL?: string
collectionLabels?: {
plural?: string
singular?: string
}
collectionSlug?: string
createProps?: {
[key: string]: unknown
style?: CSSProperties
}
devMode?: boolean
divProps?: {
[key: string]: unknown
style?: CSSProperties
}
editProps?: {
[key: string]: unknown
style?: CSSProperties
}
id?: string
logo?: ReactElement
logoProps?: {
[key: string]: unknown
style?: CSSProperties
}
logoutProps?: {
[key: string]: unknown
style?: CSSProperties
}
onAuthChange?: (user: PayloadMeUser) => void
onPreviewExit?: () => void
preview?: boolean
previewProps?: {
[key: string]: unknown
style?: CSSProperties
}
style?: CSSProperties
unstyled?: boolean
userProps?: {
[key: string]: unknown
style?: CSSProperties
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.24.0",
"version": "3.28.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.24.0",
"version": "3.28.0",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -47,7 +47,6 @@
},
"dependencies": {
"mongoose": "8.9.5",
"mongoose-aggregate-paginate-v2": "1.1.2",
"mongoose-paginate-v2": "1.8.5",
"prompts": "2.4.2",
"uuid": "10.0.0"
@@ -55,6 +54,8 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
"@types/prompts": "^2.4.5",
"@types/uuid": "10.0.0",
"mongodb": "6.12.0",
"mongodb-memory-server": "^10",
"payload": "workspace:*"

View File

@@ -70,9 +70,15 @@ export const connect: Connect = async function connect(
await this.migrate({ migrations: this.prodMigrations })
}
} catch (err) {
let msg = `Error: cannot connect to MongoDB.`
if (typeof err === 'object' && err && 'message' in err && typeof err.message === 'string') {
msg = `${msg} Details: ${err.message}`
}
this.payload.logger.error({
err,
msg: `Error: cannot connect to MongoDB. Details: ${err.message}`,
msg,
})
process.exit(1)
}

View File

@@ -6,13 +6,15 @@ import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
export const count: Count = async function count(
this: MongooseAdapter,
{ collection, locale, req, where },
{ collection: collectionSlug, locale, req, where = {} },
) {
const Model = this.collections[collection]
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
const options: CountOptions = {
session: await getSession(this, req),
}
@@ -26,8 +28,8 @@ export const count: Count = async function count(
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
collectionSlug,
fields: collectionConfig.flattenedFields,
locale,
where,
})

View File

@@ -6,13 +6,15 @@ import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getGlobal } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
this: MongooseAdapter,
{ global, locale, req, where },
{ global: globalSlug, locale, req, where = {} },
) {
const Model = this.versions[global]
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug, versions: true })
const options: CountOptions = {
session: await getSession(this, req),
}
@@ -26,11 +28,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
const query = await buildQuery({
adapter: this,
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.globals.config.find((each) => each.slug === global),
true,
),
fields: buildVersionGlobalFields(this.payload.config, globalConfig, true),
locale,
where,
})

View File

@@ -6,13 +6,19 @@ import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
export const countVersions: CountVersions = async function countVersions(
this: MongooseAdapter,
{ collection, locale, req, where },
{ collection: collectionSlug, locale, req, where = {} },
) {
const Model = this.versions[collection]
const { collectionConfig, Model } = getCollection({
adapter: this,
collectionSlug,
versions: true,
})
const options: CountOptions = {
session: await getSession(this, req),
}
@@ -26,11 +32,7 @@ export const countVersions: CountVersions = async function countVersions(
const query = await buildQuery({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
locale,
where,
})

View File

@@ -1,48 +1,53 @@
import type { CreateOptions } from 'mongoose'
import type { Create, Document } from 'payload'
import type { Create } from 'payload'
import type { MongooseAdapter } from './index.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const create: Create = async function create(
this: MongooseAdapter,
{ collection, data, req },
{ collection: collectionSlug, data, req, returning },
) {
const Model = this.collections[collection]
const { collectionConfig, customIDType, Model } = getCollection({ adapter: this, collectionSlug })
const options: CreateOptions = {
session: await getSession(this, req),
}
let doc
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
transform({
adapter: this,
data,
fields: this.payload.collections[collection].config.fields,
fields: collectionConfig.fields,
operation: 'write',
})
if (this.payload.collections[collection].customIDType) {
sanitizedData._id = sanitizedData.id
if (customIDType) {
data._id = data.id
}
try {
;[doc] = await Model.create([sanitizedData], options)
;[doc] = await Model.create([data], options)
} catch (error) {
handleError({ collection, error, req })
handleError({ collection: collectionSlug, error, req })
}
if (returning === false) {
return null
}
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
doc = doc.toObject()
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
transform({
adapter: this,
data: doc,
fields: collectionConfig.fields,
operation: 'read',
})
return result
return doc
}

View File

@@ -1,38 +1,44 @@
import type { CreateOptions } from 'mongoose'
import type { CreateGlobal } from 'payload'
import { type CreateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
import { getGlobal } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
{ slug, data, req },
{ slug: globalSlug, data, req, returning },
) {
const Model = this.globals
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug })
const global = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
globalType: slug,
...data,
},
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
transform({
adapter: this,
data,
fields: globalConfig.fields,
globalSlug,
operation: 'write',
})
const options: CreateOptions = {
session: await getSession(this, req),
}
let [result] = (await Model.create([global], options)) as any
let [result] = (await Model.create([data], options)) as any
if (returning === false) {
return null
}
result = JSON.parse(JSON.stringify(result))
result = result.toObject()
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
transform({
adapter: this,
data: result,
fields: globalConfig.fields,
operation: 'read',
})
return result
}

View File

@@ -1,11 +1,10 @@
import type { CreateOptions } from 'mongoose'
import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload'
import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { getGlobal } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter,
@@ -16,37 +15,41 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
parent,
publishedLocale,
req,
returning,
snapshot,
updatedAt,
versionData,
},
) {
const VersionModel = this.versions[globalSlug]
const options: CreateOptions = {
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug, versions: true })
const options = {
session: await getSession(this, req),
}
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
const data = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
}
const fields = buildVersionGlobalFields(this.payload.config, globalConfig)
transform({
adapter: this,
data,
fields,
operation: 'write',
})
const [doc] = await VersionModel.create([data], options, req)
let [doc] = await Model.create([data], options, req)
await VersionModel.updateMany(
await Model.updateMany(
{
$and: [
{
@@ -70,13 +73,18 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
if (returning === false) {
return null
}
return result
doc = doc.toObject()
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
return doc
}

View File

@@ -42,8 +42,9 @@ export const createMigration: CreateMigration = async function createMigration({
const migrationFileContent = migrationTemplate(predefinedMigration)
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')
const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '')
const formattedDate = yyymmdd!.replace(/\D/g, '')
const formattedTime = hhmmss!.split('.')[0]!.replace(/\D/g, '')
const timestamp = `${formattedDate}_${formattedTime}`

View File

@@ -1,12 +1,10 @@
import type { CreateOptions } from 'mongoose'
import { Types } from 'mongoose'
import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload'
import { buildVersionCollectionFields, type CreateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { transform } from './utilities/transform.js'
export const createVersion: CreateVersion = async function createVersion(
this: MongooseAdapter,
@@ -17,35 +15,43 @@ export const createVersion: CreateVersion = async function createVersion(
parent,
publishedLocale,
req,
returning,
snapshot,
updatedAt,
versionData,
},
) {
const VersionModel = this.versions[collectionSlug]
const options: CreateOptions = {
const { collectionConfig, Model } = getCollection({
adapter: this,
collectionSlug,
versions: true,
})
const options = {
session: await getSession(this, req),
}
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
),
const data = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
}
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
transform({
adapter: this,
data,
fields,
operation: 'write',
})
const [doc] = await VersionModel.create([data], options, req)
let [doc] = await Model.create([data], options, req)
const parentQuery = {
$or: [
@@ -56,15 +62,8 @@ export const createVersion: CreateVersion = async function createVersion(
},
],
}
if (data.parent instanceof Types.ObjectId) {
parentQuery.$or.push({
parent: {
$eq: data.parent.toString(),
},
})
}
await VersionModel.updateMany(
await Model.updateMany(
{
$and: [
{
@@ -89,13 +88,18 @@ export const createVersion: CreateVersion = async function createVersion(
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
if (returning === false) {
return null
}
return result
doc = doc.toObject()
transform({
adapter: this,
data: doc,
fields,
operation: 'read',
})
return doc
}

View File

@@ -1,24 +1,27 @@
import type { DeleteOptions } from 'mongodb'
import type { DeleteMany } from 'payload'
import { type DeleteMany } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: MongooseAdapter,
{ collection, req, where },
{ collection: collectionSlug, req, where },
) {
const Model = this.collections[collection]
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
const options: DeleteOptions = {
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
collectionSlug,
fields: collectionConfig.flattenedFields,
where,
})

View File

@@ -1,22 +1,24 @@
import type { QueryOptions } from 'mongoose'
import type { DeleteOne, Document } from 'payload'
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { DeleteOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter,
{ collection, req, select, where },
{ collection: collectionSlug, req, returning, select, where },
) {
const Model = this.collections[collection]
const options: QueryOptions = {
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
const options: MongooseUpdateQueryOptions = {
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
fields: collectionConfig.flattenedFields,
select,
}),
session: await getSession(this, req),
@@ -24,22 +26,28 @@ export const deleteOne: DeleteOne = async function deleteOne(
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
collectionSlug,
fields: collectionConfig.flattenedFields,
where,
})
if (returning === false) {
await Model.deleteOne(query, options)?.lean()
return null
}
const doc = await Model.findOneAndDelete(query, options)?.lean()
if (!doc) {
return null
}
let result: Document = JSON.parse(JSON.stringify(doc))
transform({
adapter: this,
data: doc,
fields: collectionConfig.fields,
operation: 'read',
})
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return doc
}

View File

@@ -3,26 +3,27 @@ import { buildVersionCollectionFields, type DeleteVersions } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
export const deleteVersions: DeleteVersions = async function deleteVersions(
this: MongooseAdapter,
{ collection, locale, req, where },
{ collection: collectionSlug, locale, req, where },
) {
const VersionsModel = this.versions[collection]
const { collectionConfig, Model } = getCollection({
adapter: this,
collectionSlug,
versions: true,
})
const session = await getSession(this, req)
const query = await buildQuery({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
locale,
where,
})
await VersionsModel.deleteMany(query, { session })
await Model.deleteMany(query, { session })
}

View File

@@ -7,15 +7,17 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { aggregatePaginate } from './utilities/aggregatePaginate.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const find: Find = async function find(
this: MongooseAdapter,
{
collection,
collection: collectionSlug,
joins = {},
limit = 0,
locale,
@@ -25,11 +27,10 @@ export const find: Find = async function find(
req,
select,
sort: sortArg,
where,
where = {},
},
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
const session = await getSession(this, req)
@@ -53,8 +54,8 @@ export const find: Find = async function find(
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
collectionSlug,
fields: collectionConfig.flattenedFields,
locale,
where,
})
@@ -108,7 +109,8 @@ export const find: Find = async function find(
if (limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
paginationOptions.options!.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
@@ -120,7 +122,7 @@ export const find: Find = async function find(
const aggregate = await buildJoinAggregation({
adapter: this,
collection,
collection: collectionSlug,
collectionConfig,
joins,
locale,
@@ -128,18 +130,30 @@ export const find: Find = async function find(
})
// build join aggregation
if (aggregate) {
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
result = await aggregatePaginate({
adapter: this,
collation: paginationOptions.collation,
joinAggregation: aggregate,
limit: paginationOptions.limit,
Model,
page: paginationOptions.page,
pagination: paginationOptions.pagination,
projection: paginationOptions.projection,
query,
session: paginationOptions.options?.session ?? undefined,
sort: paginationOptions.sort as object,
useEstimatedCount: paginationOptions.useEstimatedCount,
})
} else {
result = await Model.paginate(query, paginationOptions)
}
const docs = JSON.parse(JSON.stringify(result.docs))
transform({
adapter: this,
data: result.docs,
fields: collectionConfig.fields,
operation: 'read',
})
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
return result
}

View File

@@ -7,15 +7,17 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getGlobal } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: MongooseAdapter,
{ slug, locale, req, select, where },
{ slug: globalSlug, locale, req, select, where = {} },
) {
const Model = this.globals
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug })
const fields = globalConfig.flattenedFields
const options: QueryOptions = {
lean: true,
select: buildProjectionFromSelect({
@@ -29,23 +31,23 @@ export const findGlobal: FindGlobal = async function findGlobal(
const query = await buildQuery({
adapter: this,
fields,
globalSlug: slug,
globalSlug,
locale,
where: combineQueries({ globalType: { equals: slug } }, where),
where: combineQueries({ globalType: { equals: globalSlug } }, where),
})
let doc = (await Model.findOne(query, {}, options)) as any
const doc: any = await Model.findOne(query, {}, options)
if (!doc) {
return null
}
if (doc._id) {
doc.id = doc._id
delete doc._id
}
doc = JSON.parse(JSON.stringify(doc))
doc = sanitizeInternalFields(doc)
transform({
adapter: this,
data: doc,
fields: globalConfig.fields,
operation: 'read',
})
return doc
}

View File

@@ -1,26 +1,35 @@
import type { PaginateOptions, QueryOptions } from 'mongoose'
import type { FindGlobalVersions } from 'payload'
import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
import { APIError, buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getGlobal } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: MongooseAdapter,
{ global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where },
{
global: globalSlug,
limit,
locale,
page,
pagination,
req,
select,
skip,
sort: sortArg,
where = {},
},
) {
const Model = this.versions[global]
const versionFields = buildVersionGlobalFields(
this.payload.config,
this.payload.globals.config.find(({ slug }) => slug === global),
true,
)
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug, versions: true })
const versionFields = buildVersionGlobalFields(this.payload.config, globalConfig, true)
const session = await getSession(this, req)
const options: QueryOptions = {
@@ -91,10 +100,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
}
}
if (limit >= 0) {
if (limit && limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
paginationOptions.options!.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
@@ -103,13 +113,13 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
transform({
adapter: this,
data: result.docs,
fields: buildVersionGlobalFields(this.payload.config, globalConfig),
operation: 'read',
})
return result
}

View File

@@ -1,20 +1,23 @@
import type { AggregateOptions, QueryOptions } from 'mongoose'
import type { Document, FindOne } from 'payload'
import { type FindOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { aggregatePaginate } from './utilities/aggregatePaginate.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
{ collection, joins, locale, req, select, where },
{ collection: collectionSlug, joins, locale, req, select, where = {} },
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
const session = await getSession(this, req)
const options: AggregateOptions & QueryOptions = {
lean: true,
@@ -23,7 +26,7 @@ export const findOne: FindOne = async function findOne(
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
collectionSlug,
fields: collectionConfig.flattenedFields,
locale,
where,
@@ -37,10 +40,9 @@ export const findOne: FindOne = async function findOne(
const aggregate = await buildJoinAggregation({
adapter: this,
collection,
collection: collectionSlug,
collectionConfig,
joins,
limit: 1,
locale,
projection,
query,
@@ -48,7 +50,17 @@ export const findOne: FindOne = async function findOne(
let doc
if (aggregate) {
;[doc] = await Model.aggregate(aggregate, { session })
const { docs } = await aggregatePaginate({
adapter: this,
joinAggregation: aggregate,
limit: 1,
Model,
pagination: false,
projection,
query,
session,
})
doc = docs[0]
} else {
;(options as Record<string, unknown>).projection = projection
doc = await Model.findOne(query, {}, options)
@@ -58,11 +70,7 @@ export const findOne: FindOne = async function findOne(
return null
}
let result: Document = JSON.parse(JSON.stringify(doc))
transform({ adapter: this, data: doc, fields: collectionConfig.fields, operation: 'read' })
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
return doc
}

View File

@@ -8,15 +8,31 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { transform } from './utilities/transform.js'
export const findVersions: FindVersions = async function findVersions(
this: MongooseAdapter,
{ collection, limit, locale, page, pagination, req = {}, select, skip, sort: sortArg, where },
{
collection: collectionSlug,
limit,
locale,
page,
pagination,
req = {},
select,
skip,
sort: sortArg,
where = {},
},
) {
const Model = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config
const { collectionConfig, Model } = getCollection({
adapter: this,
collectionSlug,
versions: true,
})
const session = await getSession(this, req)
const options: QueryOptions = {
limit,
@@ -92,10 +108,11 @@ export const findVersions: FindVersions = async function findVersions(
}
}
if (limit >= 0) {
if (limit && limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
paginationOptions.options!.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
@@ -104,13 +121,13 @@ export const findVersions: FindVersions = async function findVersions(
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
transform({
adapter: this,
data: result.docs,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
operation: 'read',
})
return result
}

View File

@@ -11,11 +11,13 @@ import type {
BaseDatabaseAdapter,
CollectionSlug,
DatabaseAdapterObj,
Migration,
Payload,
TypeWithID,
TypeWithVersion,
UpdateGlobalArgs,
UpdateGlobalVersionArgs,
UpdateManyArgs,
UpdateOneArgs,
UpdateVersionArgs,
} from 'payload'
@@ -53,6 +55,7 @@ import { commitTransaction } from './transactions/commitTransaction.js'
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
import { updateGlobal } from './updateGlobal.js'
import { updateGlobalVersion } from './updateGlobalVersion.js'
import { updateMany } from './updateMany.js'
import { updateOne } from './updateOne.js'
import { updateVersion } from './updateVersion.js'
import { upsert } from './upsert.js'
@@ -60,6 +63,13 @@ import { upsert } from './upsert.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export interface Args {
/**
* By default, Payload strips all additional keys from MongoDB data that don't exist
* in the Payload schema. If you have some data that you want to include to the result
* but it doesn't exist in Payload, you can enable this flag
* @default false
*/
allowAdditionalKeys?: boolean
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
autoPluralization?: boolean
/**
@@ -86,11 +96,15 @@ export interface Args {
* Defaults to disabled.
*/
collation?: Omit<CollationOptions, 'locale'>
collectionsSchemaOptions?: Partial<Record<CollectionSlug, SchemaOptions>>
/** Extra configuration options */
connectOptions?: {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
/**
* Set false to disable $facet aggregation in non-supporting databases, Defaults to true
* @deprecated Payload doesn't use `$facet` anymore anywhere.
*/
useFacet?: boolean
} & ConnectOptions
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
@@ -105,11 +119,7 @@ export interface Args {
* typed as any to avoid dependency
*/
mongoMemoryServer?: MongoMemoryReplSet
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
prodMigrations?: Migration[]
transactionOptions?: false | TransactionOptions
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
@@ -160,6 +170,7 @@ declare module 'payload' {
updateGlobalVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateGlobalVersionArgs<T>,
) => Promise<TypeWithVersion<T>>
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
updateVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
@@ -171,11 +182,12 @@ declare module 'payload' {
}
export function mongooseAdapter({
allowAdditionalKeys = false,
autoPluralization = true,
collectionsSchemaOptions = {},
connectOptions,
disableIndexHints = false,
ensureIndexes,
ensureIndexes = false,
migrationDir: migrationDirArg,
mongoMemoryServer,
prodMigrations,
@@ -192,17 +204,22 @@ export function mongooseAdapter({
// Mongoose-specific
autoPluralization,
collections: {},
// @ts-expect-error initialize without a connection
connection: undefined,
connectOptions: connectOptions || {},
disableIndexHints,
ensureIndexes,
// @ts-expect-error don't have globals model yet
globals: undefined,
// @ts-expect-error Should not be required
mongoMemoryServer,
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
updateMany,
url,
versions: {},
// DatabaseAdapter
allowAdditionalKeys,
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
collectionsSchemaOptions,
commitTransaction,

View File

@@ -2,12 +2,15 @@ import type { PaginateOptions } from 'mongoose'
import type { Init, SanitizedCollectionConfig } from 'payload'
import mongoose from 'mongoose'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import {
buildVersionCollectionFields,
buildVersionCompoundIndexes,
buildVersionGlobalFields,
} from 'payload'
import type { MongooseAdapter } from './index.js'
import type { CollectionModel } from './types.js'
import type { CollectionModel, GlobalModel } from './types.js'
import { buildCollectionSchema } from './models/buildCollectionSchema.js'
import { buildGlobalModel } from './models/buildGlobalModel.js'
@@ -17,7 +20,7 @@ import { getDBName } from './utilities/getDBName.js'
export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schemaOptions = this.collectionsSchemaOptions[collection.slug]
const schemaOptions = this.collectionsSchemaOptions?.[collection.slug]
const schema = buildCollectionSchema(collection, this.payload, schemaOptions)
@@ -37,6 +40,7 @@ export const init: Init = function init(this: MongooseAdapter) {
},
...schemaOptions,
},
compoundIndexes: buildVersionCompoundIndexes({ indexes: collection.sanitizedIndexes }),
configFields: versionCollectionFields,
payload: this.payload,
})
@@ -48,10 +52,6 @@ export const init: Init = function init(this: MongooseAdapter) {
}),
)
if (Object.keys(collection.joins).length > 0) {
versionSchema.plugin(mongooseAggregatePaginate)
}
const versionCollectionName =
this.autoPluralization === true && !collection.dbName ? undefined : versionModelName
@@ -59,21 +59,21 @@ export const init: Init = function init(this: MongooseAdapter) {
versionModelName,
versionSchema,
versionCollectionName,
) as CollectionModel
) as unknown as CollectionModel
}
const modelName = getDBName({ config: collection })
const collectionName =
this.autoPluralization === true && !collection.dbName ? undefined : modelName
this.collections[collection.slug] = mongoose.model(
this.collections[collection.slug] = mongoose.model<any>(
modelName,
schema,
collectionName,
) as CollectionModel
})
this.globals = buildGlobalModel(this.payload)
this.globals = buildGlobalModel(this.payload) as GlobalModel
this.payload.config.globals.forEach((global) => {
if (global.versions) {
@@ -101,7 +101,7 @@ export const init: Init = function init(this: MongooseAdapter) {
}),
)
this.versions[global.slug] = mongoose.model(
this.versions[global.slug] = mongoose.model<any>(
versionModelName,
versionSchema,
versionModelName,

View File

@@ -1,5 +1,3 @@
import type { PayloadRequest } from 'payload'
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
import prompts from 'prompts'

View File

@@ -1,7 +1,6 @@
import type { PaginateOptions, Schema } from 'mongoose'
import type { Payload, SanitizedCollectionConfig } from 'payload'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
@@ -24,12 +23,13 @@ export const buildCollectionSchema = (
...schemaOptions,
},
},
compoundIndexes: collection.sanitizedIndexes,
configFields: collection.fields,
payload,
})
if (Array.isArray(collection.upload.filenameCompoundIndex)) {
const indexDefinition: Record<string, 1> = collection.upload.filenameCompoundIndex.reduce(
const indexDefinition = collection.upload.filenameCompoundIndex.reduce<Record<string, 1>>(
(acc, index) => {
acc[index] = 1
return acc
@@ -44,12 +44,5 @@ export const buildCollectionSchema = (
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
if (
Object.keys(collection.joins).length > 0 ||
Object.keys(collection.polymorphicJoins).length > 0
) {
schema.plugin(mongooseAggregatePaginate)
}
return schema
}

File diff suppressed because it is too large Load Diff

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