Compare commits

..

89 Commits

Author SHA1 Message Date
Sasha
3722308c3d write test 2025-05-27 17:48:33 +03:00
Sasha
b61ef13481 fix(storage-vercel-blob): client uploads with a prefix (#12559)
Fixes https://github.com/payloadcms/payload/issues/12544
2025-05-26 22:42:25 +03:00
Paul
1731dd7c36 fix: improve translation script prompt and fix some incorrectly used terms in spanish and dutch (#12548)
The translations would sometimes be using the wrong meanings in other
languages, for example Locale becoming "location" or "region" depending
on the phrase or translation.

Some words were also not being used consistently across the UI which
could cause some confusion if they're interchanged.

I've fixed these instances for locale specifically in dutch and spanish.

I've also updated the prompt with more context around what's being
translated and some examples, over time we should add to the examples so
that translations are better guarded against changing meanings.

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-05-26 11:07:13 -03:00
Anders Semb Hermansen
bd2571c68f fix(translations): correct Norwegian terms for “locale” and language labels (#12557)
The translation engine previously rendered the English term locale as
“lokalitet” or “steder” in Norwegian, which in practice refers to a
geographic location rather than a language variant. This has been
changed throughout. I have also made some other small improvements to
the Norwegian translations.
2025-05-26 13:59:31 +00:00
Anyu Jiang
e2f7889d72 docs: fix typos, duplicated words, wrong property names etc. (#12480)
### What?
fix typos in doc
### Why?
because they are typos
### How?
checked manually with the help of AI
2025-05-25 10:58:26 -07:00
Jarrod Flesch
c010d51543 fix: browseByFolder route should be optional (#12527) 2025-05-23 15:45:03 -04:00
Jarrod Flesch
293cdc1b50 fix(ui): safari css rendering issues with table and folder cards (#12531) 2025-05-23 15:43:39 -04:00
Dan Ribbens
06fbc0705c chore: fixes monorepo dev plugin-import-export (#12528) 2025-05-23 18:42:50 +00:00
Jarrod Flesch
5a758810aa fix(ui): only and files/folders to the grid/list if they were added to the current folder (#12525) 2025-05-23 14:04:57 -04:00
Jarrod Flesch
64443d83ec fix: thread req into interal folder payload operations (#12523) 2025-05-23 12:19:57 -04:00
Jarrod Flesch
feeee19407 fix(ui): replaces css fn with css calc (#12520) 2025-05-23 11:09:15 -04:00
Jarrod Flesch
e9cda1e121 fix(ui): index based ids without useAsTitle breaks folders (#12519) 2025-05-23 10:37:40 -04:00
Ricardo Tavares
842d1845e1 fix: infinite loop in findUp utility when a result is not found (#12457)
### What?
Fixes an infinite loop that may occur when invoking the findUp() utility
to search for a file by name and no result is found.

### Why?
If triggered, the infinite loop will hang the entire application. For
example, this occurs when trying to boot Payload from a Cloudflare
Worker, as reported in #12327

### How?
By checking whether it has reached the root directory before analysing
the parent folder
2025-05-23 03:27:09 -07:00
Said Akhrarov
11a4a20f0f perf: folder views download only images and get best fit from image sizes (#12514) 2025-05-22 20:29:48 -04:00
Jarrod Flesch
bc43982cfc fix(next): folder redirects not working (#12509) 2025-05-22 16:21:58 -04:00
Jarrod Flesch
d83b2bf3fa chore: moves collections folders property to the top level (#12508) 2025-05-22 16:01:33 -04:00
Jacob Fletcher
f75d62c79b feat: select field filter options (#12487)
It is a common pattern to dynamically show and validate a select field's
options based on various criteria such as the current user or underlying
document.

Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g.
a city/state selector
 
While this is already possible to do with a custom `validate` function,
the user can still view and select the forbidden option...unless you
_also_ wired up a custom component.

Now, you can define `filterOptions` on select fields.

This behaves similarly to the existing `filterOptions` property on
relationship and upload fields, except the return value of this function
is simply an array of options, not a query constraint. The result of
this function will determine what is shown to the user and what is
validated on the server.

Here's an example:

```ts
{
  name: 'select',
  type: 'select',
  options: [
    {
      label: 'One',
      value: 'one',
    },
    {
      label: 'Two',
      value: 'two',
    },
    {
      label: 'Three',
      value: 'three',
    },
  ],
  filterOptions: ({ options, data }) =>
    data.disallowOption1
      ? options.filter(
          (option) => (typeof option === 'string' ? options : option.value) !== 'one',
        )
      : options,
}
```
2025-05-22 15:54:12 -04:00
Sasha
45f4c5c22c fix: delete subfolders hook with relational databases (#12507)
* Adds integration tests for folder view hooks
* Fixes deleting subfolders with Postgres / SQLite by changing
`afterDelete` to `beforeDelete`.
2025-05-22 19:44:14 +00:00
Jake Grella
071c61fe49 chore(examples): remove unused imports from custom server example (#12467)
Removed `express` imports that were not being utilized.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-05-22 18:20:47 +00:00
Rémy
cceb793257 chore(examples): fix read permission in auth example (#12403)
The return value of the `adminsAndUser` method was not a proper Query to
limit the read scope of the read access. So users could read all user
data of the system.

Alongside I streamlined the type imports (fixes #12323) and fixed some
typescript typings. And aligned the export of the mentioned to align
with the other access methods.
2025-05-22 17:46:27 +00:00
Sasha
1b1e36e2df fix(db-*): migrate:reset executes in a wrong order (#12445)
fixes https://github.com/payloadcms/payload/issues/12442
2025-05-22 13:30:29 -04:00
Elliot DeNolf
6b6948f92c templates: bump for v3.39.1 (#12504)
🤖 Automated bump of templates for v3.39.1

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-22 11:51:05 -04:00
Elliot DeNolf
9ef51a7cf3 chore(release): v3.39.1 [skip ci] 2025-05-22 11:37:58 -04:00
Elliot DeNolf
0f7dc38012 fix: update folders export paths (#12501)
Fixes issues with folder exports after generating import map

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-05-22 11:35:00 -04:00
Dan Ribbens
c720ce3c08 docs: folders beta (#12500) 2025-05-22 11:25:56 -04:00
Elliot DeNolf
3a73a67ef4 templates: include ui package (#12499)
Folder view needed to have `@payloadcms/ui` explicitly installed.
Including this in the templates.
2025-05-22 11:19:49 -04:00
Elliot DeNolf
4c6fde0e89 templates: bump for v3.39.0 (#12498)
🤖 Automated bump of templates for v3.39.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-22 10:32:52 -04:00
Elliot DeNolf
c1c0db3b01 chore(release): v3.39.0 [skip ci] 2025-05-22 10:18:04 -04:00
Jarrod Flesch
00667faf8d feat: folders (#10030) 2025-05-22 10:04:45 -04:00
Paul
898e97ed17 fix(cpa): ensure it always installs the latest version of the templates (#12488)
CPA would previously install an outdated version of the templates based
on the git tag, this is now set to the `main` branch ensuring that the
latest version is always installed.
2025-05-22 09:55:42 -04:00
Jarrod Flesch
8142a00da6 chore: simplifies buildColumnState functions (#12496) 2025-05-22 09:54:50 -04:00
Anders Semb Hermansen
08a3dfbbcb chore(live-preview): load schemaJSON from proper client config in integration tests (#12167)
### What?

The integration tests in live-preview has been using the
`fieldSchemaToJSON` method with wrong params/types.

It's defined as
```
export const fieldSchemaToJSON = (fields: ClientField[], config: ClientConfig): FieldSchemaJSON
```

In the test setup
`fields` was set to `Pages.fields` which was `Field[]`, not the expected
`ClientField[]`
`config` was set to `config` which was `Promise<SanitizedConfig>` not
the expected `ClientConfig`

### Why?

I'm working on some other changes to live-preview where I need the
proper values wired up correctly to properly add integration tests.

The test has worked up until now because Field is very similar to
ClientField. But it should test with the correct type.

### How?

By creating the clientConfig and using the correct types/params when
calling fieldSchemaToJSON in the test setup.

**Note:** Removed test "Collections - Live Preview › merges data", the
test worked before because **id** field is not part of Field, but part
of ClientField. So test code does not behave like this in real scenario
when real ClientField is used. There are lots of real tests for correct
data, removed this one which seems very simple and not correct.
2025-05-22 07:44:03 -03:00
Germán Jabloñski
fc83823e5d feat(richtext-lexical): add TextStateFeature (allows applying styles such as color and background color to text) (#9667)
Originally this PR was going to introduce a `TextColorFeature`, but it
ended up becoming a more general-purpose `TextStateFeature`.

## Example of use:
```ts
import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical'

TextStateFeature({
  // prettier-ignore
  state: {
    color: {
      ...defaultColors,
      // fancy gradients!
      galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } },
      sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } },
    },
    // You can have both colored and underlined text at the same time. 
    // If you don't want that, you should group them within the same key.
    // (just like I did with defaultColors and my fancy gradients)
    underline: {
      'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } },
       // You'll probably want to use the CSS light-dark() utility.
      'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } },
    },
  },
}),

```

Which will result in the following:


![image](https://github.com/user-attachments/assets/ed29b30b-8efd-4265-a1b9-125c97ac5fce)


## Challenges & Considerations
Adding colors or styles in general to the Lexical editor is not as
simple as it seems.

1. **Extending TextNode isn't ideal**
- While possible, it's verbose, error-prone, and not composable. If
multiple features extend the same node, conflicts arise.
- That’s why we collaborated with the Lexical team to introduce [the new
State API](https://lexical.dev/docs/concepts/node-replacement)
([PR](https://github.com/facebook/lexical/pull/7117)).
2. **Issues with patchStyles**
- Some community plugins use `patchStyles`, but storing CSS in the
editor’s JSON has drawbacks:
- Style adaptability: Users may want different styles per scenario
(dark/light mode, mobile/web, etc.).
- Migration challenges: Hardcoded colors (e.g., #FF0000) make updates
difficult. Using tokens (e.g., "red") allows flexibility.
      - Larger JSON footprint increases DB size.
3. **Managing overlapping styles**
- Some users may want both text and background colors on the same node,
while others may prefer mutual exclusivity.
    - This approach allows either:
        - Using a single "color" state (e.g., "bg-red" + "text-red").
- Defining separate "bg-color" and "text-color" states for independent
styling.
4. **Good light and dark modes by default**
- Many major editors (Google Docs, OneNote, Word) treat dark mode as an
afterthought, leading to poor UX.
- We provide a well-balanced default palette that looks great in both
themes, serving as a strong foundation for customization.
5. **Feature name. Why TextState?**
- Other names considered were `TextFormatFeature` and
`TextStylesFeature`. The term `format` in Lexical and Payload is already
used to refer to something else (italic, bold, etc.). The term `style`
could be misleading since it is never attached to the editorState.
    - State seems appropriate because:
      - Lexical's new state API is used under the hood.
- Perhaps in the future we'll want to make state features for other
nodes, such as `ElementStateFeature` or `RootStateFeature`.

Note: There's a bug in Lexical's `forEachSelectedTextNode`. When the
selection includes a textNode partially on the left, all state for that
node is removed instead of splitting it along the selection edge.
2025-05-21 23:58:17 +00:00
Anders Semb Hermansen
2a41d3fbb1 feat: show fields inside groups as separate columns in the list view (#7355)
## Description

Group fields are shown as one column, this PR changes this so that the
individual field is now shown separately.

Before change:
<img width="1227" alt="before change"
src="https://github.com/user-attachments/assets/dfae58fd-8ad2-4329-84fd-ed1d4eb20854">

After change:
<img width="1229" alt="after change"
src="https://github.com/user-attachments/assets/d4fd78bb-c474-436e-a0f5-cac4638b91a4">

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

## Type of change

- [X] New feature (non-breaking change which adds functionality)

## Checklist:

- [X] I have added tests that prove my fix is effective or that my
feature works
- [X] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
2025-05-21 16:25:34 -04:00
Patrik
c772a3207c fix(ui): set gap to 0 in sort column buttons to remove unneeded spacing (#12481)
### What

This PR adjusts the `gap` between buttons in the `SortColumn` component.
The previous spacing (`calc(var(--base) / 4)`) caused too much visual
separation between the sort buttons. It has been replaced with `gap: 0`
to tighten their alignment.

#### Before:
![Screenshot 2025-05-21 at 1 33
17 PM](https://github.com/user-attachments/assets/a5f759fc-647a-46e3-8dac-e3e100fc7b98)

#### After:
![Screenshot 2025-05-21 at 1 34
04 PM](https://github.com/user-attachments/assets/29572620-bd62-4e3e-80b7-d32ed4c81911)
2025-05-21 11:33:14 -07:00
Germán Jabloñski
c701dd41a9 docs: update rich text to HTML conversion documentation (#12465)
Fixes #8168, #8277

The fact that `lexicalHTMLField` doesn't work with live preview was
already clarified at the beginning of the page. I mentioned it again in
the dedicated section because it seems there was still confusion.

Also, I reordered and hierarchized the headings correctly. The
introduction said there were two ways to convert to HTML, but there were
four headings with the same level. I also made the headings a little
shorter to make the table of contents easier to parse.
2025-05-21 15:24:31 -03:00
Paul
4dfb2d24bb feat(plugin-form-builder): add new date field (#12416)
Adds a new date field to take submission values for.

It can help form serialisers render the right input for this kind of
field as the submissions themselves don't do any validation right now.

Disabled by default as to not cause any conflicts with existing projects
potentially inserting their own date blocks.

Can be enabled like this

```ts
formBuilderPlugin({
   fields: {
     date: true
   }
})
```
2025-05-21 17:34:21 +00:00
Sasha
230128b92e fix(db-mongodb): remove limit from nested querying (#12464)
Fixes https://github.com/payloadcms/payload/issues/12456
2025-05-21 20:22:28 +03:00
Dan Ribbens
23f42040ab chore: ignore .idea run configuration templates (#12439)
Webstorm run configuration template files are trying to sneak into my
commits.
2025-05-21 13:13:55 -04:00
Alessio Gravili
8596ac5694 fix(richtext-lexical): support inline block types in strict mode for JSXConvertersFunction type (#12478)
Same as https://github.com/payloadcms/payload/pull/10398 but for inline
blocks.

> Reproduction steps:
> 1. Set `strict: true` in `templates/website/tsconfig.json`
> 2. You will find a ts error in
`templates/website/src/components/RichText/index.tsx`.
> 
> This is because the blockType property of blocks is generated by
Payload as a literal (e.g. "mediaBlock") and cannot be assigned to a
string.
> 
> To test this PR, you can make the change to `JSXConvertersFunction` in
node_modules of the website template
2025-05-21 16:54:03 +00:00
Keisuke Ikeda
324daff553 docs: fix API capitalization typo in virtual fields documentation (#12477) 2025-05-21 15:56:58 +00:00
Jacob Fletcher
22b1858ee8 fix: auto inject req.user into query preset constraints (#12461)
In #12322 we prevented against accidental query preset lockout by
throwing a validation error when the user is going to change the preset
in a way that removes their own access to it. This, however, puts the
responsibility on the user to make the corrections and is an unnecessary
step.

For example, the API currently forbids leaving yourself out of the
`users` array when specifying the `specificUsers` constraint, but when
you encounter this error, have to update the field manually and try
again.

To improve the experience, we now automatically inject the requesting
user onto the `users` array when this constraint is selected. This will
guarantee they have access and prevent an accidental lockout while also
avoiding the API error feedback loop.
2025-05-20 17:15:18 -04:00
conico974
2ab8e2e194 fix: telemetry in opennext cloudflare (#12327)
<!--

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?
This PR help to fix an issue you'll encounter while running payload in
OpenNext on cloudflare

### Why?
Sending telemetry event will create an infinite loop because it won't be
able to find a `package.json`

### How?
Putting the whole logic of `sendEvent` behind `config.telemetry` allows
to disable it and thus, make it work on cloudflare

See this comment for more info :
https://github.com/opennextjs/opennextjs-cloudflare/issues/263#issuecomment-2851747956
2025-05-20 07:49:23 -07:00
Patrik
1235a183ff fix: prevent resizing of original file with withoutEnlargement on update (#12291)
This PR updates `generateFileData` to skip applying `resizeOptions`
after updating an image if `resizeOptions.withoutEnlargement` is `true`
and the original image size is smaller than the dimensions defined in
`resizeOptions`.

This prevents unintended re-resizing of already resized images when
updating or modifying metadata without uploading a new file.

This change ensures that:

- Resizing is skipped if withoutEnlargement: true

- Resizing still occurs if withoutEnlargement: false or unset

This resolves an issue where images were being resized again
unnecessarily when updating an upload.

Fixes #12280
2025-05-20 06:43:53 -07:00
Sasha
81d333f4b0 test: add test for sorting by a virtual field with a reference (#12351) 2025-05-20 13:07:48 +00:00
Sasha
4fe3423e54 fix(plugin-multi-tenant): multi-locale tenant select label (#12444)
fixes https://github.com/payloadcms/payload/issues/12443
2025-05-20 05:02:47 -07:00
Paul
e8c2b15e2b fix(plugin-multi-tenant): add missing translation for Assigned Tenant field (#12448)
Previously the "Assigned Tenant" field didn't have a translated label
2025-05-19 13:20:12 -07:00
Paul
3127d6ad6d fix(plugin-import-export): add translations for all UI elements and fields (#12449)
Converts all text and field labels into variables that can be
translated. Also generated the translations for them

So now the UI here is internationalised


![image](https://github.com/user-attachments/assets/40d7c010-ac58-4cd7-8786-01b3de3cabb7)

I've also moved some of the generic labels into the core package since
those could be re-used elsewhere
2025-05-19 13:19:55 -07:00
Paul
72ab319d37 fix(db-*): ensure consistent sorting even when sorting on non-unique fields or no sort parameters at all (#12447)
The databases do not keep track of document order internally so when
sorting by non-unique fields such as shared `order` number values, the
returned order will be random and not consistent.

While this issue is far more noticeable on mongo it could also occur in
postgres on certain environments.

This combined with pagination can lead to the perception of duplicated
or inconsistent data.

This PR adds a second sort parameter to queries so that we always have a
fallback, `-createdAt` will be used by default or `id` if timestamps are
disabled.
2025-05-19 12:59:12 -07:00
Germán Jabloñski
2a929cf385 chore: fix all lint errors and add mechanisms to prevent them from appearing again (#12401)
I think it's easier to review this PR commit by commit, so I'll explain
it this way:

## Commits
1. [parallelize eslint script (still showing logs results in
serial)](c9ac49c12d):
Previously, `--concurrency 1` was added to the script to make the logs
more readable. However, turborepo has an option specifically for these
use cases: `--log-order=grouped` runs the tasks in parallel but outputs
them serially. As a result, the lint script is now significantly faster.
2. [run pnpm
lint:fix](9c128c276a)
The auto-fix was run, which resolved some eslint errors that were
slipped in due to the use of `no-verify`. Most of these were
`perfectionist` fixes (property ordering) and the removal of unnecessary
assertions. Starting with this PR, this won't happen again in the
future, as we'll be verifying the linter in every PR across the entire
codebase (see commit 7).
3. [fix eslint non-autofixable
errors](700f412a33)
All manual errors have been resolved except for the configuration errors
addressed in commit 5. Most were React compiler violations, which have
been disabled and commented out "TODO" for now. There's also an unused
`use no memo` and a couple of `require` errors.
4. [move react-compiler linter to eslint-config
package](4f7cb4d63a)
To simplify the eslint configuration. My concern was that there would be
a performance regression when used in non-react related packages, but
none was experienced. This is probably because it only runs on .tsx
files.
5. [remove redundant eslint config files and fix
allowDefaultProject](a94347995a)
The main feature introduced by `typescript-eslint` v8 was
`projectService`, which automatically searches each file for the closest
`tsconfig`, greatly simplifying configuration in monorepos
([source](https://typescript-eslint.io/blog/announcing-typescript-eslint-v8#project-service)).
Once I moved `projectService` to `packages/eslint-config`, all the other
configuration files could be easily removed.
I confirmed that pnpm lint still works on individual packages.
The other important change was that the pending eslint errors from
commits 2 and 3 were resolved. That is, some files were giving the
error: "[File] was not found by the project service. Consider either
including it in the tsconfig.json or including it in
allowDefaultProject." Below I copy the explanatory comment I left in the
code:
```ts
// This is necessary because `tsconfig.base.json` defines `"rootDir": "${configDir}/src"`,
// And the following files aren't in src because they aren't transpiled.
// This is typescript-eslint's way of adding files that aren't included in tsconfig.
// See: https://typescript-eslint.io/troubleshooting/typed-linting/#i-get-errors-telling-me--was-not-found-by-the-project-service-consider-either-including-it-in-the-tsconfigjson-or-including-it-in-allowdefaultproject
// The best practice is to have a tsconfig.json that covers ALL files and is used for
// typechecking (with noEmit), and a `tsconfig.build.json` that is used for the build
// (or alternatively, swc, tsup or tsdown). That's what we should ideally do, in which case
// this hardcoded list wouldn't be necessary. Note that these files don't currently go
// through ts, only through eslint.
```

6. [Differentiate errors from warnings in VScode ESLint
Rules](5914d2f48d)
There's no reason to do that. If an eslint rule isn't an error, it
should be disabled or converted to a warning.
7. [Disable skip lint, and lint over the entire repo now that it's
faster](e4b28f1360)
The GitHub action linted only the files that had changed in the PR.
While this seems like a good idea, once exceptions were introduced with
[skip lint], they opened the door to propagating more and more errors.
Often, the linter was skipped, not because someone introduced new
errors, but because they were trying to avoid those that had already
crept in, sometimes accidentally introducing new ones.
On the other hand, `pnpm lint` now runs in parallel (commit 1), so it's
not that slow. Additionally, it runs in parallel with other GitHub
actions like e2e tests, which take much longer, so it can't represent a
bottleneck in CI.
8. [fix lint in next
package](4506595f91)
Small fix missing from commit 5
9. [Merge remote-tracking branch 'origin/main' into
fix-eslint](563d4909c1)
10. [add again eslint.config.js in payload
package](78f6ffcae7)
The comment in the code explains it. Basically, after the merge from
main, the payload package runs out of memory when linting, probably
because it grew in recent PRs. That package will sooner or later
collapse for our tooling, so we may have to split it. It's already too
big.

## Future Actions
- Resolve React compiler violations, as mentioned in commit 3.
- Decouple the `tsconfig` used for typechecking and build across the
entire monorepo (as explained in point 5) to ensure ts coverage even for
files that aren't transpiled (such as scripts).
- Remove the few remaining `eslint.config.js`. I had to leave the
`richtext-lexical` and `next` ones for now. They could be moved to the
root config and scoped to their packages, as we do for example with
`templates/vercel-postgres/**`. However, I couldn't get it to work, I
don't know why.
- Make eslint in the test folder usable. Not only are we not linting
`test` in CI, but now the `pnpm eslint .` command is so large that my
computer freezes. If each suite were its own package, this would be
solved, and dynamic codegen + git hooks to modify tsconfig.base.json
wouldn't be necessary
([related](https://github.com/payloadcms/payload/pull/11984)).
2025-05-19 12:36:40 -03:00
Sasha
38029cdd6e chore(drizzle): fix lint errors in @payloadcms/drizzle (#12428) 2025-05-19 08:14:20 +00:00
Femi Oladipo
14252696ce fix: incorrect environment file loading (#12360)
### What?

Fixes issue with the Payload CLI where environment files were silently
always loaded as if in development mode, even when `NODE_ENV=production`
is explicitly set. Achieved by dynamically checking the enviroment based
on `process.env.NODE_ENV` (defaulting to "development") then passing
that to the underlying library `@next/env`.

### Why?

Previously, the Payload CLI always passed `true` to the `dev` flag of
`loadEnvConfig` from `@next/env`, causing it to load
development-specific `.env` files even when `NODE_ENV=production` was
explicitly set. Frustratingly for the user there was also no warning
message that this was happening.

For example, previously when running:
```sh
NODE_ENV=production pnpm payload run ./seed.ts
```
It would still load `.env.development*` and not `.env.production*`.

The inability to override the dev flag previously made it difficult, bar
impossible (depending on ones setup), to run the CLI in a
production-like environment. Which is useful for several reasons, a few
examples being:
- Seeding production data:
```
NODE_ENV=production payload run seed.ts
```
- Capturing current schema:
```
NODE_ENV=production payload migrate:create
```
- Running one-off jobs with live data:
```
NODE_ENV=production payload run jobs/consolidate-payments.ts
```

This fix allows users to correctly target production without surprises.

### How?

- Introduced a dev constant that checks `NODE_ENV !== 'production'`
- Passed dev to `loadEnvConfig` to allow `@next/env` to resolve the
correct `.env.*` files based on the environment:

**Before:**
```ts
const { loadedEnvFiles } = loadEnvConfig(process.cwd(), true) // assuming this won't run in production
```
**After:**
```ts
const dev = process.env.NODE_ENV !== 'production'
const { loadedEnvFiles } = loadEnvConfig(process.cwd(), dev)
```

The signature of `loadEnvConfig` from
[packages/next-env/index.ts](2086975c3c/packages/next-env/index.ts (L114))
is:
```ts
export function loadEnvConfig(
  dir: string,
  dev?: boolean,
  log: Log = console,
  forceReload = false,
  onReload?: (envFilePath: string) => void
): {
  combinedEnv: Env
  parsedEnv: Env | undefined
  loadedEnvFiles: LoadedEnvFiles
} 
```

Logic from `loadEnvConfig` in
[packages/next-env/index.ts](2086975c3c/packages/next-env/index.ts (L136))
that handles loading dependant on the `dev` variable:
```ts
const mode = isTest ? 'test' : dev ? 'development' : 'production'
const dotenvFiles = [
  `.env.${mode}.local`,
  mode !== 'test' && `.env.local`,
  `.env.${mode}`,
  '.env',
]
```

This change allows Payload CLI commands to honor the current `NODE_ENV`,
loading `.env.production*`, as intended when running with
`NODE_ENV=production`.

No behavioral changes for existing dev users, but adds expected support
for production workflows.
---
Note:
There are a few consideration I made here.
1. Default to development if not explicitly set (I think most would
agree).
2. I haven't implemented warning for non standard `NODE_ENV` values, as
the `Next.js` cli does. For example `NODE_ENV=alpha`.
3. Extension of point 2, I haven't implemented loading of non-standard
`NODE_ENV` values.

I do believe either point 2 or 3 should be implemented however. So users
are not left surprised by the actions of the CLI.

FYI, the `Next.js` cli does the warning in the main package and not
`@next/env`. This logic exists in
[packages/next/src/bin/next.ts](2086975c3c/packages/next/src/bin/next.ts (L64)):

```ts
const standardEnv = ['production', 'development', 'test']

if (process.env.NODE_ENV) {
  const isNotStandard = !standardEnv.includes(process.env.NODE_ENV)
  const shouldWarnCommands =
    process.env.NODE_ENV === 'development'
      ? ['start', 'build']
      : process.env.NODE_ENV === 'production'
        ? ['dev']
        : []

  if (isNotStandard || shouldWarnCommands.includes(commandName)) {
    warn(NON_STANDARD_NODE_ENV)
  }
}
```
This warns when using the wrong `NODE_ENV` for a command (I don't think
that applies here?). Also warning when a non-standard `NODE_ENV` is used
(e.g., `NODE_ENV=alpha`) and will not attempt to load `.env.alpha*`.
This makes unexpected behaviour non-silent.

If desired, I can add the warning to this PR or follow up with a
separate PR. However, loading non-standard `NODE_ENV` is a bigger
discussion and implementation. That I would prefer to leave out of this
PR so not to block it moving along. But I felt it was worth mentioning.
2025-05-18 23:23:41 +00:00
Jacob Fletcher
5855f3a475 fix: sanitize duplicate blocks (#12440) 2025-05-17 09:20:28 -04:00
Germán Jabloñski
529bfe149e fix: orderable with groups and tabs requires migration (#12422)
⚠️ `orderable` fields will no longer be `required` and `unique`, so your
database may prompt you to accept an automatic migration if you're using
[this
feature](https://payloadcms.com/docs/configuration/collections#config-options).
Note that the `orderable` feature is still experimental, so it may still
receive breaking changes without a major upgrade or contain bugs. Use it
with caution.
___

The `orderable` fields will not have `required` and `unique` constraints
at the database schema level, in order to automatically migrate
collections that incorporate this property.

Now, when a user adds the `orderable` property to a collection or join
field, existing documents will have the order field set to undefined.
The first time you try to reorder them, the documents will be
automatically assigned an initial order, and you will be prompted to
refresh the page.

We believe this provides a better development experience than having to
manually migrate data with a script.

Additionally, it fixes a bug that occurred when using `orderable` in
conjunction with groups and tabs fields.

Closes:
- #12129
- #12331
- #12212

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-05-16 22:21:46 +00:00
Jacob Fletcher
18f2f899c5 perf(ui): useAsTitle field lags on slow cpu (#12436)
When running the Payload admin panel on a machine with a slower CPU,
form state lags significantly and can become nearly unusable or even
crash when interacting with the document's `useAsTitle` field.

Here's an example:


https://github.com/user-attachments/assets/3535fa99-1b31-4cb6-b6a8-5eb9a36b31b7

#### Why this happens

The reason for this is that entire React component trees are
re-rendering on every keystroke of the `useAsTitle` field, twice over.

Here's a breakdown of the flow:

1. First, we dispatch form state events to the form context. Only the
components that are subscribed to form state re-render when this happens
(good).
2. Then, we sync the `useAsTitle` field to the document info provider,
which lives outside the form. Regardless of whether its children need to
be aware of the document title, all components subscribed to the
document info context will re-render (there are many, including the form
itself).

Given how far up the rendering tree the document info provider is, its
rendering footprint, and the rate of speed at which these events are
dispatched, this is resource intensive.

#### What is the fix

The fix is to isolate the document's title into it's own context. This
way only the components that are subscribed to specifically this context
will re-render as the title changes.

Here's the same test with the same CPU throttling, but no lag:


https://github.com/user-attachments/assets/c8ced9b1-b5f0-4789-8d00-a2523d833524
2025-05-16 15:51:57 -04:00
Germán Jabloñski
d4899b84cc fix(templates): make images visible in live preview if it is not running on port 3000 (#12432)
I couldn't find much information on the internet about
`__NEXT_PRIVATE_ORIGIN`, but I could observe that when port 3000 was
busy and 3001 was used, `NEXT_PUBLIC_SERVER_URL` was
`http://localhost:3000`, while `__NEXT_PRIVATE_ORIGIN` was
`http://localhost:3001`.

Fixes #12431
2025-05-16 13:57:57 -03:00
Anyu Jiang
6fb2beb983 fix(ui): render missing group children fields for unnamed group (#12433)
### What?
Basically an unnamed group moves all of its children to the same level
with the group. When another field at the same level has a unique access
setting, the permissions will return a json of permissions for each
fields at the same level instead of return a default `true` value. For
traditional group field, there will be a `fields` property inside the
permissions object, so it can use ```permissions={permissions === true ?
permissions : permissions?.fields``` as the attribution of
<RenderFields> in `packages/ui/src/fields/Group/index.tsx`. Right now,
since we somehow "promote" the group's children to the upper level,
which makes the `fields` property no longer exists in the `permissions`
object. Hence, the `permissions?.fields` mentioned above will always be
undefined, which will lead to return null for this field, because the
getFieldPermissions will always get read permission as undefined.

### Why?
The only reason we use `permissions : permissions?.fields` before
because the traditional group field moves all its children to a child
property `fields`. Since we somehow promoted those children to upper
level, so there is no need to access the fields property anymore.

### How?
For the permissions attribute for unnamed group's <RenderFields>, simple
pass in `permissions={permissions}` instead of `{permissions === true ?
permissions : permissions?.fields}`, since you have already gotten all
you want in permissions. No worry about the extra permission property
brought in(the access permission in the unnamed group level), because
`getFieldPermissions` will filter those redundant ones out.

Fixes #12430
2025-05-16 16:00:26 +00:00
Elliot DeNolf
4166621966 templates: bump for v3.38.0 (#12434)
🤖 Automated bump of templates for v3.38.0

Triggered by user: @paulpopus

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-16 08:46:56 -07:00
Paul
e395a0aa66 chore: add ignores .next folder in eslint config for templates template (#12423)
The automated PR will override this config in other templates, so I'm
just copying it into the base template eslint config

```
 {
    ignores: ['.next/'],
  },
```
2025-05-16 10:47:05 -04:00
ch-jwoo
cead312d4b fix(plugin-seo): fix genImageResponse result parsing (#12301)
<!--

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?
`Auto-generate` button of Meta image doesn't work

### Why?
`/plugin-seo/generate-image` return imageId as below when using
`genImageResponse.text()`.
"\"result\":\"68139a9d0effac229865fbc9\""

### How?
Change `text()` to `json()` to parse the response.
2025-05-16 09:50:12 -04:00
Sasha
219fd01717 fix(db-postgres): allow the same block slug in different places with a different localized value (#12414)
Fixes https://github.com/payloadcms/payload/issues/12409
Now Payload automatically resolves table names conflicts in those cases,
as well as Drizzle relation names.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-05-15 16:48:41 -04:00
Sasha
1f6efe9a46 fix: respect hidden: true for virtual fields that have reference to a relationship field (#12219)
Previously, `hidden: true` on a virtual field that references a
relationship field didn't work. Now, this field doesn't get calculated
if there's `hidden: true` and no `showHiddenFields` was passed.

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-05-15 16:48:08 -04:00
Jarrod Flesch
88769c8244 feat(ui): extracts relationship input for external use (#12339) 2025-05-15 14:54:26 -04:00
Jarrod Flesch
bd6ee317c1 fix(ui): req not being threaded through to views (#12213) 2025-05-15 14:49:37 -04:00
Elliot DeNolf
561708720d chore(release): v3.38.0 [skip ci] 2025-05-15 14:39:34 -04:00
Sasha
58fc2f9a74 fix(db-postgres): build near sort query properly for point fields (#12240)
Continuation of https://github.com/payloadcms/payload/pull/12185 and fix
https://github.com/payloadcms/payload/issues/12221

The mentioned PR introduced auto sorting by the point field when a
`near` query is used, but it didn't build actual needed query to order
results by their distance to a _given_ (from the `near` query) point.

Now, we build:
```sql
order by pont_field <-> ST_SetSRID(ST_MakePoint(lng, lat), 4326)
```

Which does what we want
2025-05-15 13:45:33 -04:00
Sasha
5fce501589 fix(db-postgres): dbName in arrays regression with long generated drizzle relation names (#12237)
Fixes https://github.com/payloadcms/payload/issues/12136 which caused by
regression from https://github.com/payloadcms/payload/pull/11995

The previous PR solved an issue where the generated drizzle relation
name was too long because of Payload field names, for example
```
{
  name: 'thisIsALongFieldNameThatWillCauseAPostgresErrorEvenThoughWeSetAShorterDBName',
  dbName: 'shortname',
  type: 'array',
  fields: [
    {
      name: 'nested_field_1',
      type: 'array',
      dbName: 'short_nested_1',
      fields: [],
    },
    {
      name: 'nested_field_2',
      type: 'text',
    },
  ],
},
```
But it caused regression, when custom `dbName` vice versa caused long
relation names:
```
export const Header: GlobalConfig = {
  slug: 'header',
  fields: [
    {
      name: 'itemsLvl1',
      type: 'array',
      dbName: 'header_items_lvl1',
      fields: [
        {
          name: 'label',
          type: 'text',
        },
        {
          name: 'itemsLvl2',
          type: 'array',
          dbName: 'header_items_lvl2',
          fields: [
            {
              name: 'label',
              type: 'text',
            },
            {
              name: 'itemsLvl3',
              type: 'array',
              dbName: 'header_items_lvl3',
              fields: [
                {
                  name: 'label',
                  type: 'text',
                },
                {
                  name: 'itemsLvl4',
                  type: 'array',
                  dbName: 'header_items_lvl4',
                  fields: [
                    {
                      name: 'label',
                      type: 'text',
                    },
                  ],
                },
              ],
            },
          ],
        },
      ],
    },
  ],
}
```

Notice if you calculate the generated relation name for `itemsLvl4` you
get:

`header__header_items_lvl1__header_items_lvl2__header_items_lvl3_header_items_lvl4`
- 81 characters, Drizzle, for joining shrink the alias to 63 characters
-`header__header_items_lvl1__header_items_lvl2__header_items_lvl3` and
Postgres throws:
```
error: table name "header__header_items_lvl1__header_items_lvl2__header_items_lvl3" specified more than once
```
2025-05-15 13:40:24 -04:00
Paul
3e7db302ee fix(richtext-lexical): newTab not being able to be checked to true by default (#12389)
Previously the value of new tab checkbox in the link feature was not
able to be set to true by default because we were passing `false` as a
default value.

This fixes that and adds test coverage for customising that link drawer.
2025-05-15 15:57:23 +00:00
Jarrod Flesch
7498d09f1c fix(next): tells webpack not to bundle the require-in-the-middle pkg (#12417) 2025-05-15 11:21:23 -04:00
Dan Ribbens
3edfd7cc6d fix(db-postgres): v2-v3 migration errors with relation already exists (#12310)
This fixes issues identified in the predefined migration for
postgres v2-v3 including the following:


### relation already exists
Can error with the following: 
```ts
{
  err: [DatabaseError],
  msg: 'Error running migration 20250502_020052_relationships_v2_v3 column "relation_id" of relation "table_name" already exists.'
}
```
This was happening when you run a migration with both a required
relationship or upload field and no schema specified in the db adapter.
When both of these are true the function that replaces `ADD COLUMN` and
`ALTER COLUMN` in order to add `NOT NULL` constraints for requried
fields, wasn't working. This resulted in the `ADD COLUMN` statement from
being being called multiple times instead of altering it after data had
been copied over.

### camelCase column change

Enum columns from using `select` or `radio` have changed from camelCase
to snake case in v3. This change was not accounted for in the
relationship migration and needed to be accounted for.

### DROP CONSTRAINT

It was pointed out by
[here](https://github.com/payloadcms/payload/issues/10162#issuecomment-2610018940)
that the `DROP CONSTRAINT` needs to include `IF EXISTS` so that it can
continue if the contraint was already removed in a previous statement.

fixes https://github.com/payloadcms/payload/issues/10162
2025-05-15 09:02:15 -04:00
Dmitrijs Trifonovs
77bb7e3638 feat: add latvian language support (#12363)
This PR adds Latvian language support, based on the instructions
provided in the documentation
2025-05-15 03:15:50 +00:00
Sasha
8ebadd4190 fix(ui): respect filterOptions: { id: { in: [] } } (#12408)
Fixes the issue where this returns all the documents:
```
{
  name: 'post',
  type: 'relationship',
  relationTo: 'posts',
  filterOptions: { id: { in: [] } }
}
```

The issue isn't with the Local API but with how we send the query to the
REST API through `qs.stringify`. `qs.stringify({ id: { in: [] } }`
becomes `""`, so the server ignores the original query. I don't think
it's possible to encode empty arrays with this library
https://github.com/sindresorhus/query-string/issues/231, so I just made
sanitization to `{ exists: false }` for this case.
2025-05-14 22:13:15 -04:00
Paul
e258cd73ef feat: allow group fields to have an optional name (#12318)
Adds the ability to completely omit `name` from group fields now so that
they're entirely presentational.

New config:
```ts
import type { CollectionConfig } from 'payload'

export const ExampleCollection: CollectionConfig = {
  slug: 'posts',
  fields: [
    {
      label: 'Page header',
      type: 'group', // required
      fields: [
        {
          name: 'title',
          type: 'text',
          required: true,
        },
      ],
    },
  ],
}
```

will create
<img width="332" alt="image"
src="https://github.com/user-attachments/assets/10b4315e-92d6-439e-82dd-7c815a844035"
/>


but the data response will still be

```
{
    "createdAt": "2025-05-05T13:42:20.326Z",
    "updatedAt": "2025-05-05T13:42:20.326Z",
    "title": "example post",
    "id": "6818c03ce92b7f92be1540f0"

}
```

Checklist:
- [x] Added int tests
- [x] Modify mongo, drizzle and graphql packages
- [x] Add type tests
- [x] Add e2e tests
2025-05-14 23:45:34 +00:00
Alessio Gravili
d63c8baea5 fix(plugin-cloud): ensure scheduled publishing works if no custom jobs are defined (#12410)
Previously, plugin-cloud would only set up job auto-running if a job configuration was present in the custom config at initialization time.

However, some jobs - such as the scheduled publish job which is added during sanitization - are added after plugin-cloud has initialized. This means relying solely on the initial state of the job config is insufficient for determining whether to enable auto-running.

This PR removes that check and ensures auto-running is always initialized, allowing later-added jobs to run as expected.

## Weakening type

This PR also weakens to `config.jobs.tasks` type and makes that property optional. It's totally permissible to only have workflows that define inline tasks, and to not have any static tasks defined in `config.jobs.tasks`. Thus it makes no sense to make that property required.
2025-05-14 21:58:25 +00:00
Jacob Fletcher
93d79b9c62 perf: remove duplicative deep loops during field sanitization (#12402)
Optimizes the field sanitization process by removing duplicative deep
loops over the config. We were previously iterating over all fields of
each collection potentially multiple times in order validate field
configs, check reserved field names, etc. Now, we perform all necessary
sanitization within a single loop.
2025-05-14 15:25:44 -04:00
Jacob Fletcher
9779cf7f7d feat: prevent query preset lockout (#12322)
Prevents an accidental lockout of query preset documents. An "accidental
lockout" occurs when the user sets access control on a preset and
excludes themselves. This can happen in a variety of scenarios,
including:

 - You select `specificUsers` without specifying yourself
- You select `specificRoles` without specifying a role that you are a
part of
 - Etc.

#### How it works

To make this happen, we use a custom validation function that executes
access against the user's proposed changes. If those changes happen to
remove access for them, we throw a validation error and prevent that
change from ever taking place. This means that only a user with proper
access can remove another user from the preset. You cannot remove
yourself.

To do this, we create a temporary record in the database that we can
query against. We use transactions to ensure that the temporary record
is not persisted once our work is completed. Since not all Payload
projects have transactions enabled, we flag these temporary records with
the `isTemp` field.

Once created, we query the temp document to determine its permissions.
If any of the operations throw an error, this means the user can no
longer act on them, and we throw a validation error.

#### Alternative Approach
 
A previous approach that was explored was to add an `owner` field to the
presets collection. This way, the "owner" of the preset would be able to
completely bypass all access control, effectively eliminating the
possibility of a lockout event.

But this doesn't work for other users who may have update access. E.g.
they could still accidentally remove themselves from the read or update
operation, preventing them from accessing that preset after submitting
the form. We need a solution that works for all users, not just the
owner.
2025-05-14 19:25:32 +00:00
Ruslan
b7b2b390fc feat(ui): fixed toolbar group customization (#12108)
### What

This PR introduces a comprehensive customization system for toolbar
groups in the Lexical Rich Text Editor. It allows developers to override
not just the order, but virtually any aspect of toolbar components (such
as format, align, indent) through the `FixedToolbarFeature`
configuration. Customizable properties include order, icons, group type,
and more.

### Why

Previously, toolbar group configurations were hardcoded in their
respective components with no way to modify them without changing the
source code. This made it difficult for developers to:

1. Reorder toolbar components to match specific UX requirements
2. Replace icons with custom ones to maintain design consistency 
3. Transform dropdown groups into button groups or vice versa
4. Apply other customizations needed for specific projects

This enhancement provides full flexibility for tailoring the rich text
editor interface while maintaining a clean and maintainable codebase.

### How

The implementation consists of three key parts:

1. **Enhanced the FixedToolbarFeature API**:
- Added a new `customGroups` property to `FixedToolbarFeatureProps` that
accepts a record mapping group keys to partial `ToolbarGroup` objects
- These partial objects can override any property of the default toolbar
group configuration

2. **Leveraged existing deep merge utility**:
- Used Payload's existing `deepMerge` utility to properly combine
default configurations with custom overrides
- This ensures that only specified properties are overridden while
preserving all other default behaviors
3. **Applied customizations in the sanitization process**:
- Updated the `sanitizeClientFeatures` function to identify and apply
custom group configurations
- Applied deep merging before the sorting process to ensure proper
ordering with customized configurations
- Maintained backward compatibility for users who don't need
customization

### Usage Example

```typescript
import { FixedToolbarFeature } from '@payloadcms/richtext-lexical'
import { CustomIcon } from './icons/CustomIcon'

{
  name: 'content',
  type: 'richText',
  admin: {
    features: [
      // Other features...
      FixedToolbarFeature({
        customGroups: {
            'text': {
              order: 10,
              ChildComponent: CustomIcon,
            },
            'format': {
              order: 15,
            },
            'add': {
              type: 'buttons',
              order: 20,
            },
        }
      })
    ]
  }
}
```

### Demo


https://github.com/user-attachments/assets/c3a59b60-b6c2-4721-bbc0-4954bdf52625

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-05-14 19:25:02 +00:00
Jacob Fletcher
7130834152 feat: thread overrideAccess through field validations (#12399)
Threads the `overrideAccess` property through the field-level
validations. This way custom `validate` functions can be aware of its
value and adjust their logic accordingly.

See #12322 for an example use case.
2025-05-14 14:10:46 -04:00
Philipp Schneider
1d5d96d2c3 perf: actually debounce rich text editor field value updates to only process latest state (#12086)
Follow-up work to #12046, which was misnamed. It improved UI
responsiveness of the rich text field on CPU-limited clients, but didn't
actually reduce work by debouncing. It only improved scheduling.

Using `requestIdleCallback` lead to better scheduling of change event
handling in the rich text editor, but on CPU-starved clients, this leads
to a large backlog of unprocessed idle callbacks. Since idle callbacks
are called by the browser in submission order, the latest callback will
be processed last, potentially leading to large time delays between a
user typing, and the form state having been updated. An example: When a
user types "I", and the change events for the character "I" is scheduled
to happen in the next browser idle time, but then the user goes on to
type "love Payload", there will be 12 more callbacks scheduled. On a
slow system it's preferable if the browser right away only processes the
event that has the full editor state "I love Payload", instead of only
processing that after 11 other idle callbacks.

So this code change keeps track when requesting an idle callback and
cancels the previous one when a new change event with an updated editor
state occurs.
2025-05-14 13:14:29 -03:00
Jarrod Flesch
faa7794cc7 feat(plugin-multi-tenant): prompt the user to confirm the change of tenant before actually updating (#12382) 2025-05-14 09:45:00 -04:00
Anyu Jiang
98283ca18c fix(db-postgres): ensure module augmentation for generated schema is picked up correctly in turborepo (#12312)
### What?
Turborepo fails to compile due to type error in the generated drizzle
schema.
### Why?
TypeScript may not include the module augmentation for
@payloadcms/db-postgres, especially in monorepo or isolated module
builds. This causes type errors during the compilation process of
turborepo project. Adding the type-only import guarantees that
TypeScript loads the relevant type definitions and augmentations,
resolving these errors.
### How?
This PR adds a type-only import statement to ensure TypeScript
recognizes the module augmentation for @payloadcms/db-postgres in the
generated drizzle schema from payload, and there is no runtime effect.

Fixes #12311

-->

![image](https://github.com/user-attachments/assets/cdec275c-c062-4eb7-9e6a-c3bc3871dd65)
2025-05-13 11:23:27 -07:00
Paul
e93d0baf89 chore: add NODE_OPTIONS to vscode settings by default in the repo for playwright extension (#12390)
The official playwright extension when using the debug button to run
tests in debug mode doesn't pick up the `tests/test.env` file as
expected.

I've added the same `NODE_OPTIONS` to the vscode settings JSON for this
extension which fixes an error when running e2e tests in debug mode.
2025-05-13 10:31:06 -07:00
Paul
cd455741e5 docs: remove link to outdated ecommerce template from stripe plugin docs (#12353)
Closes https://github.com/payloadcms/payload/issues/12347

We previously linked to a non existent ecommerce template and example
from the stripe plugin docs.
2025-05-13 13:20:46 -04:00
Paul
735d699804 chore: add no-frozen-lockfile flag for templates script (#12394) 2025-05-13 07:15:38 -07:00
Jessica Rynkar
d9c0c43154 fix(ui): passes value to server component args (#12352)
### What?
Allows the field value (if defined) to be accessed from `args` with
custom server components.

### Why?
Documentation states that the user can access `args.value` to get the
value of the field at time of render (if a value is defined) when using
a custom server component - however this isn't currently setup.

<img width="469" alt="Screenshot 2025-05-08 at 4 51 30 PM"
src="https://github.com/user-attachments/assets/9c167f80-5c5e-4fea-a31c-166281d9f7db"
/>

Link to docs
[here](https://payloadcms.com/docs/fields/overview#default-props).

### How?
Passes the value from `data` if it exists (does not exist for all field
types) and adds `value` to the server component types as an optional
property.

Fixes #10389
2025-05-13 11:13:23 +01:00
Jacob Fletcher
a9cc747038 docs: add local api instructions for vercel content link (#12385)
The docs for Vercel Content Link only included instructions on how to
enable content source maps for the REST API. The Local API, although
supported, was lacking documentation.
2025-05-12 17:06:37 -04:00
Sasha
fd67d461ac fix(db-mongodb): sort by fields in relationships with draft: true (#12387)
Fixes sorting by fields in relationships, e.g `sort: "author.name"` when
using `draft: true`. The existing test that includes check with `draft:
true` was accidentally passing because it used to sort by the
relationship field itself.
2025-05-12 22:35:16 +03:00
Sasha
8219c046de fix(db-postgres): selectDistinct might remove expected rows when querying with nested fields or relations (#12365)
Fixes https://github.com/payloadcms/payload/issues/12263
This was caused by passing not needed columns to the `SELECT DISTINCT`
query, which we execute in case if we have a filter / sort by a nested
field / relationship. Since the only columns that we need to pass to the
`SELECT DISTINCT` query are: ID and field(s) specified in `sort`, we now
filter the `selectFields` variable.
2025-05-12 12:34:15 -07:00
Paul
021932cc8b chore: bump node version in monorepo and add new flag for node 23.6+ (#12328)
This PR does two things:
- Adds a new ` --no-experimental-strip-types` flag to the playwright
test env
- This is needed since 23.6.0 automatically enables this flag by default
and it breaks e2e tests
- Bumps the tooling config files to use node 23.11.0
2025-05-12 09:41:18 -04:00
780 changed files with 33319 additions and 8104 deletions

View File

@@ -6,7 +6,7 @@ inputs:
node-version:
description: Node.js version
required: true
default: 22.6.0
default: 23.11.0
pnpm-version:
description: Pnpm version
required: true

View File

@@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: true
env:
NODE_VERSION: 22.6.0
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

View File

@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
NODE_VERSION: 22.6.0
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

View File

@@ -12,7 +12,7 @@ on:
default: ''
env:
NODE_VERSION: 22.6.0
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

View File

@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
NODE_VERSION: 22.6.0
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ package-lock.json
dist
/.idea/*
!/.idea/runConfigurations
/.idea/runConfigurations/_template*
!/.idea/payload.iml
# Custom actions

View File

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

View File

@@ -1 +1 @@
v22.6.0
v23.11.0

2
.nvmrc
View File

@@ -1 +1 @@
v22.6.0
v23.11.0

View File

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

14
.vscode/launch.json vendored
View File

@@ -63,6 +63,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts query-presets",
"cwd": "${workspaceFolder}",
"name": "Run Dev Query Presets",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
"cwd": "${workspaceFolder}",
@@ -111,6 +118,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts folder-view",
"cwd": "${workspaceFolder}",
"name": "Run Dev Folder View",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts localization",
"cwd": "${workspaceFolder}",

View File

@@ -21,5 +21,8 @@
"runtimeArgs": ["--no-deprecation"]
},
// Essentially disables bun test buttons
"bun.test.filePattern": "bun.test.ts"
"bun.test.filePattern": "bun.test.ts",
"playwright.env": {
"NODE_OPTIONS": "--no-deprecation --no-experimental-strip-types"
}
}

View File

@@ -39,7 +39,7 @@ const GlobalWithAccessControl: GlobalConfig = {
update: ({ req: { user } }) => {...},
// Version-enabled Globals only
readVersion: () => {...},
readVersions: () => {...},
},
// highlight-end
}
@@ -64,7 +64,7 @@ If a Global supports [Versions](../versions/overview), the following additional
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can read this global based on its current properties.
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../configuration/globals):
To add read Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
@@ -72,7 +72,7 @@ import { GlobalConfig } from 'payload'
const Header: GlobalConfig = {
// ...
// highlight-start
read: {
access: {
read: ({ req: { user } }) => {
return Boolean(user)
},

View File

@@ -65,7 +65,7 @@ preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlig
The Preview feature can be used to achieve "Draft Preview". After clicking the preview button from the Admin Panel, you can enter into "draft mode" within your front-end application. This will allow you to adjust your page queries to include the `draft: true` param. When this param is present on the request, Payload will send back a draft document as opposed to a published one based on the document's `_status` field.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here very from framework to framework although the underlying concept is the same.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here vary from framework to framework although the underlying concept is the same.
### Next.js

View File

@@ -605,7 +605,7 @@ return (
textField: {
initialValue: 'Updated text',
valid: true,
value: 'Upddated text',
value: 'Updated text',
},
},
// blockType: "yourBlockSlug",
@@ -875,7 +875,7 @@ Useful to retrieve info about the currently logged in user as well as methods fo
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
| **`refreshPermissions`** | Load new permissions (useful when content that effects permissions has been changed) |
| **`refreshPermissions`** | Load new permissions (useful when content that affects permissions has been changed) |
| **`permissions`** | The permissions of the current user |
```tsx
@@ -1143,7 +1143,7 @@ This is useful for scenarios where you need to trigger another fetch regardless
Route transitions are useful in showing immediate visual feedback to the user when navigating between pages. This is especially useful on slow networks when navigating to data heavy or process intensive pages.
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions dy default.
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions by default.
```tsx
import { Link } from '@payloadcms/ui'

View File

@@ -62,7 +62,7 @@ In this scenario, if your cookie was still valid, malicious-intent.com would be
### CSRF Prevention
Define domains that your trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this:
Define domains that you trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this:
```ts
// payload.config.ts
@@ -102,8 +102,8 @@ If option 1 isn't possible, then you can get around this limitation by [configur
```
SameSite: None // allows the cookie to cross domains
Secure: true // ensures its sent over HTTPS only
HttpOnly: true // ensures its not accessible via client side JavaScript
Secure: true // ensures it's sent over HTTPS only
HttpOnly: true // ensures it's not accessible via client side JavaScript
```
Configuration example:

View File

@@ -71,7 +71,7 @@ export const Customers: CollectionConfig = {
#### generateEmailSubject
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML.
```ts
import type { CollectionConfig } from 'payload'
@@ -178,7 +178,7 @@ The following arguments are passed to the `generateEmailHTML` function:
#### generateEmailSubject
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML.
```ts
import type { CollectionConfig } from 'payload'

View File

@@ -38,7 +38,7 @@ const request = await fetch('http://localhost:3000', {
### Omitting The Token
In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponse` to `true` like so:
In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponses` to `true` like so:
```ts
import type { CollectionConfig } from 'payload'
@@ -46,7 +46,7 @@ import type { CollectionConfig } from 'payload'
export const UsersWithoutJWTs: CollectionConfig = {
slug: 'users-without-jwts',
auth: {
removeTokenFromResponse: true, // highlight-line
removeTokenFromResponses: true, // highlight-line
},
}
```

View File

@@ -67,7 +67,7 @@ query {
}
```
Document access can also be queried on a collection/global basis. Access on a global can queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`.
Document access can also be queried on a collection/global basis. Access on a global can be queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`.
## Me

View File

@@ -71,7 +71,7 @@ export const Admins: CollectionConfig = {
</Banner>
<Banner type="warning">
**Note:** Auth-enabled Collections with be automatically injected with the
**Note:** Auth-enabled Collections will be automatically injected with the
`hash`, `salt`, and `email` fields. [More
details](../fields/overview#field-names).
</Banner>

View File

@@ -8,7 +8,7 @@ keywords: authentication, config, configuration, documentation, Content Manageme
During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appended to the request for you.
### Definining Token Data
### Defining Token Data
You can specify what data gets encoded to the Cookie/JWT-Token by setting `saveToJWT` property on fields within your auth collection.

View File

@@ -132,6 +132,7 @@ The following options are available:
| `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. |
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
@@ -176,7 +177,7 @@ The following options are available:
#### Edit View Options
```ts
import type { CollectionCOnfig } from 'payload'
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
@@ -274,7 +275,7 @@ You can also pass an object to the collection's `graphQL` property, which allows
## TypeScript
You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizeCollectionConfig`.
You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizedCollectionConfig`.
The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload.

View File

@@ -205,7 +205,7 @@ You can also pass an object to the global's `graphQL` property, which allows you
## TypeScript
You can import types from Payload to help make writing your Global configs easier and type-safe. There are two main types that represent the Global Config, `GlobalConfig` and `SanitizeGlobalConfig`.
You can import types from Payload to help make writing your Global configs easier and type-safe. There are two main types that represent the Global Config, `GlobalConfig` and `SanitizedGlobalConfig`.
The `GlobalConfig` type represents a raw Global Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedGlobalConfig` type represents a Global Config after it has been fully sanitized. Generally, this is only used internally by Payload.

View File

@@ -84,6 +84,7 @@ The following options are available:
| **`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. |
| `folders` | An optional object to configure global folder settings. [More details](../folders/overview). |
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
| **`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. |
@@ -212,7 +213,7 @@ For more information about what we track, take a look at our [privacy policy](/p
## Cross-origin resource sharing (CORS)#cors
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or a object with the following properties:
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or an object with the following properties:
| Option | Description |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
@@ -291,7 +292,7 @@ export const script = async (config: SanitizedConfig) => {
collection: 'pages',
data: { title: 'my title' },
})
payload.logger.info('Succesffully seeded!')
payload.logger.info('Successfully seeded!')
process.exit(0)
}
```

View File

@@ -59,7 +59,7 @@ _For details on how to build Custom Views, including all available props, see [B
### Document Root
The Document Root is mounted on the top-level route for a Document. Setting this property will completely take over the entire Document View layout, including the title, [Document Tabs](#ocument-tabs), _and all other nested Document Views_ including the [Edit View](./edit-view), API View, etc.
The Document Root is mounted on the top-level route for a Document. Setting this property will completely take over the entire Document View layout, including the title, [Document Tabs](#document-tabs), _and all other nested Document Views_ including the [Edit View](./edit-view), API View, etc.
When setting a Document Root, you are responsible for rendering all necessary components and controls, as no document controls or tabs would be rendered. To replace only the Edit View precisely, use the `edit.default` key instead.

View File

@@ -40,7 +40,7 @@ The following options are available:
| `beforeDashboard` | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. [More details](#beforedashboard). |
| `beforeLogin` | An array of Custom Components to inject into the built-in Login, _before_ the default login form. [More details](#beforelogin). |
| `beforeNavLinks` | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. [More details](#beforenavlinks). |
| `graphics.Icon` | The simplified logo used in contexts like the the `Nav` component. [More details](#graphicsicon). |
| `graphics.Icon` | The simplified logo used in contexts like the `Nav` component. [More details](#graphicsicon). |
| `graphics.Logo` | The full logo used in contexts like the `Login` view. [More details](#graphicslogo). |
| `header` | An array of Custom Components to be injected above the Payload header. [More details](#header). |
| `logout.Button` | The button displayed in the sidebar that logs the user out. [More details](#logoutbutton). |
@@ -345,7 +345,7 @@ export default function MyCustomIcon() {
The `Logo` property is the full logo used in contexts like the `Login` view. This is typically a larger, more detailed representation of your brand.
To add a custom logo, use the `admin.components.graphic.Logo` property in your Payload Config:
To add a custom logo, use the `admin.components.graphics.Logo` property in your Payload Config:
```ts
import { buildConfig } from 'payload'

View File

@@ -39,7 +39,7 @@ export default buildConfig({
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
export default buildConfig({
// Automatically uses proces.env.POSTGRES_URL if no options are provided.
// Automatically uses process.env.POSTGRES_URL if no options are provided.
db: vercelPostgresAdapter(),
// Optionally, can accept the same options as the @vercel/postgres package.
db: vercelPostgresAdapter({
@@ -224,7 +224,7 @@ Make sure Payload doesn't overlap table names with its collections. For example,
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
```ts

View File

@@ -189,7 +189,7 @@ Make sure Payload doesn't overlap table names with its collections. For example,
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
```ts

View File

@@ -80,7 +80,7 @@ export const MyArrayField: Field = {
}
```
The Array Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Array Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------------- | ----------------------------------------------------------------------------------- |

View File

@@ -78,7 +78,7 @@ 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:
The Blocks Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ---------------------- | -------------------------------------------------------------------------- |

View File

@@ -68,7 +68,7 @@ export const MyCodeField: Field = {
}
```
The Code Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Code Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -58,7 +58,7 @@ export const MyCollapsibleField: Field = {
}
```
The Collapsible Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Collapsible Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------- | ------------------------------- |

View File

@@ -65,7 +65,7 @@ export const MyDateField: Field = {
}
```
The Date Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Date Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -65,7 +65,7 @@ export const MyEmailField: Field = {
}
```
The Email Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Email Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------------ | ------------------------------------------------------------------------- |

View File

@@ -35,9 +35,9 @@ export const MyGroupField: Field = {
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Required when name is undefined, defaults to name converted to words. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
@@ -69,7 +69,7 @@ export const MyGroupField: Field = {
}
```
The Group Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Group Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -86,7 +86,7 @@ export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'pageMeta', // required
name: 'pageMeta',
type: 'group', // required
interfaceName: 'Meta', // optional
fields: [
@@ -110,3 +110,38 @@ export const ExampleCollection: CollectionConfig = {
],
}
```
## Presentational group fields
You can also use the Group field to create a presentational group of fields. This is useful when you want to group fields together visually without affecting the data structure.
The label will be required when a `name` is not provided.
```ts
import type { CollectionConfig } from 'payload'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
label: 'Page meta',
type: 'group', // required
fields: [
{
name: 'title',
type: 'text',
required: true,
minLength: 20,
maxLength: 100,
},
{
name: 'description',
type: 'textarea',
required: true,
minLength: 40,
maxLength: 160,
},
],
},
],
}
```

View File

@@ -67,7 +67,7 @@ export const MyJSONField: Field = {
}
```
The JSON Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The JSON Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -70,7 +70,7 @@ export const MyNumberField: Field = {
}
```
The Number Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Number Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------------ | --------------------------------------------------------------------------------- |

View File

@@ -100,7 +100,7 @@ Here are the available Presentational Fields:
### Virtual Fields
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the APi response through hooks, etc.
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the API response through hooks, etc.
Here are the available Virtual Fields:

View File

@@ -36,7 +36,7 @@ export const MyRadioField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `label` string and a `value` string. |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
@@ -82,7 +82,7 @@ export const MyRadioField: Field = {
}
```
The Radio Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Radio Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -86,7 +86,7 @@ export const MyRelationshipField: Field = {
}
```
The Relationship Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Relationship Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -54,6 +54,7 @@ export const MySelectField: Field = {
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`filterOptions`** | Dynamically filter which options are available based on the user, data, etc. [More details](#filterOptions) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/fields/relationship#linking-virtual-fields-with-relationships). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
@@ -67,6 +68,61 @@ _\* An asterisk denotes that a property is required._
used as a GraphQL enum.
</Banner>
### filterOptions
Used to dynamically filter which options are available based on the user, data, etc.
Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g. a city/state selector
The result of `filterOptions` will determine:
- Which options are displayed in the Admin Panel
- Which options can be saved to the database
To do this, use the `filterOptions` property in your [Field Config](./overview):
```ts
import type { Field } from 'payload'
export const MySelectField: Field = {
// ...
// highlight-start
type: 'select',
options: [
{
label: 'One',
value: 'one',
},
{
label: 'Two',
value: 'two',
},
{
label: 'Three',
value: 'three',
},
],
filterOptions: ({ options, data }) =>
data.disallowOption1
? options.filter(
(option) =>
(typeof option === 'string' ? options : option.value) !== 'one',
)
: options,
// highlight-end
}
```
<Banner type="warning">
**Note:** This property is similar to `filterOptions` in
[Relationship](./relationship) or [Upload](./upload) fields, except that the
return value of this function is simply an array of options, not a query
constraint.
</Banner>
## Admin Options
To customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
@@ -83,7 +139,7 @@ export const MySelectField: Field = {
}
```
The Select Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Select Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -70,7 +70,7 @@ export const MyTextField: Field = {
}
```
The Text Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Text Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -67,7 +67,7 @@ export const MyTextareaField: Field = {
}
```
The Textarea Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Textarea Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |

103
docs/folders/overview.mdx Normal file
View File

@@ -0,0 +1,103 @@
---
title: Folders
label: Overview
order: 10
desc: Folders allow you to group documents across collections, and are a great way to organize your content.
keywords: folders, folder, content organization
---
Folders allow you to group documents across collections, and are a great way to organize your content. Folders are built on top of relationship fields, when you enable folders on a collection, Payload adds a hidden relationship field `folders`, that relates to a folder — or no folder. Folders also have the `folder` field, allowing folders to be nested within other folders.
The configuration for folders is done in two places, the collection config and the Payload config. The collection config is where you enable folders, and the Payload config is where you configure the global folder settings.
<Banner type="warning">
**Note:** The Folders feature is currently in beta and may be subject to
change in minor versions updates prior to being stable.
</Banner>
## Folder Configuration
On the payload config, you can configure the following settings under the `folders` property:
```ts
// Type definition
type RootFoldersConfiguration = {
/**
* An array of functions to be ran when the folder collection is initialized
* This allows plugins to modify the collection configuration
*/
collectionOverrides?: (({
collection,
}: {
collection: CollectionConfig
}) => CollectionConfig | Promise<CollectionConfig>)[]
/**
* Ability to view hidden fields and collections related to folders
*
* @default false
*/
debug?: boolean
/**
* The Folder field name
*
* @default "folder"
*/
fieldName?: string
/**
* Slug for the folder collection
*
* @default "payload-folders"
*/
slug?: string
}
```
```ts
// Example usage
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
folders: {
// highlight-start
debug: true, // optional
collectionOverrides: [
async ({ collection }) => {
return collection
},
], // optional
fieldName: 'folder', // optional
slug: 'payload-folders', // optional
// highlight-end
},
})
```
## Collection Configuration
To enable folders on a collection, you need to set the `admin.folders` property to `true` on the collection config. This will add a hidden relationship field to the collection that relates to a folder — or no folder.
```ts
// Type definition
type CollectionFoldersConfiguration = boolean
```
```ts
// Example usage
import { buildConfig } from 'payload'
const config = buildConfig({
collections: [
{
slug: 'pages',
// highlight-start
folders: true, // defaults to false
// highlight-end
},
],
})
```

View File

@@ -81,7 +81,7 @@ To install a Database Adapter, you can run **one** of the following commands:
#### 2. Copy Payload files into your Next.js app folder
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](<https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)>) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
```plaintext
app/

View File

@@ -23,7 +23,7 @@ Let's see examples on how context can be used in the first two scenarios mention
### Passing Data Between Hooks
To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it the context in a later hook.
To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it in the context of a later hook.
For example:

View File

@@ -63,19 +63,50 @@ const config = buildConfig({
export default config
```
Now in your Next.js app, include the `?encodeSourceMaps=true` parameter in any of your API requests. For performance reasons, this should only be done when in draft mode or on preview deployments.
## Enabling Content Source Maps
Now in your Next.js app, you need to add the `encodeSourceMaps` query parameter to your API requests. This will tell Payload to include the Content Source Maps in the API response.
<Banner type="warning">
**Note:** For performance reasons, this should only be done when in draft mode
or on preview deployments.
</Banner>
#### REST API
If you're using the REST API, include the `?encodeSourceMaps=true` search parameter.
```ts
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
const res = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?where[slug][equals]=${slug}&encodeSourceMaps=true`,
`${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?encodeSourceMaps=true&where[slug][equals]=${slug}`,
)
}
```
#### Local API
If you're using the Local API, include the `encodeSourceMaps` via the `context` property.
```ts
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
const res = await payload.find({
collection: 'pages',
where: {
slug: {
equals: slug,
},
},
context: {
encodeSourceMaps: true,
},
})
}
```
And that's it! You are now ready to enter Edit Mode and begin visually editing your content.
#### Edit Mode
## Edit Mode
To see Content Link on your site, you first need to visit any preview deployment on Vercel and login using the Vercel Toolbar. When Content Source Maps are detected on the page, a pencil icon will appear in the toolbar. Clicking this icon will enable Edit Mode, highlighting all editable fields on the page in blue.
@@ -94,7 +125,9 @@ const { cleaned, encoded } = vercelStegaSplit(text)
### Blocks and array fields
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site. You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are automatically given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site.
You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
```ts
<div data-vercel-edit-target>

View File

@@ -33,7 +33,7 @@ Simply add a task to the `jobs.tasks` array in your Payload config. A task consi
| `onSuccess` | Function to be executed if the task succeeds. |
| `retries` | Specify the number of times that this step should be retried if it fails. If this is undefined, the task will either inherit the retries from the workflow or have no retries. If this is 0, the task will not be retried. By default, this is undefined. |
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks picks up a Job that includes this task.
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks up a Job that includes this task.
It should return an object with an `output` key, which should contain the output of the task as you've defined.
@@ -213,7 +213,7 @@ export default buildConfig({
## Nested tasks
You can run sub-tasks within an existing task, by using the `tasks` or `ìnlineTask` arguments passed to the task `handler` function:
You can run sub-tasks within an existing task, by using the `tasks` or `inlineTask` arguments passed to the task `handler` function:
```ts
export default buildConfig({

View File

@@ -260,7 +260,7 @@ If you are using relationships or uploads in your front-end application, and you
{
// ...
// If your site is running on a different domain than your Payload server,
// This will allows requests to be made between the two domains
// This will allow requests to be made between the two domains
cors: [
'http://localhost:3001' // Your front-end application
],

View File

@@ -85,6 +85,7 @@ formBuilderPlugin({
checkbox: true,
number: true,
message: true,
date: false,
payment: false,
},
})
@@ -349,6 +350,18 @@ Maps to a `checkbox` input on your front-end. Used to collect a boolean value.
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
### Date
Maps to a `date` input on your front-end. Used to collect a date value.
| Property | Type | Description |
| -------------- | -------- | ---------------------------------------------------- |
| `name` | string | The name of the field. |
| `label` | string | The label of the field. |
| `defaultValue` | date | The default value of the field. |
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
### Number
Maps to a `number` input on your front-end. Used to collect a number.
@@ -421,6 +434,42 @@ formBuilderPlugin({
})
```
### Customizing the date field default value
You can custommise the default value of the date field and any other aspects of the date block in this way.
Note that the end submission source will be responsible for the timezone of the date. Payload only stores the date in UTC format.
```ts
import { fields as formFields } from '@payloadcms/plugin-form-builder'
// payload.config.ts
formBuilderPlugin({
fields: {
// date: true, // just enable it without any customizations
date: {
...formFields.date,
fields: [
...(formFields.date && 'fields' in formFields.date
? formFields.date.fields.map((field) => {
if ('name' in field && field.name === 'defaultValue') {
return {
...field,
timezone: true, // optionally enable timezone
admin: {
...field.admin,
description: 'This is a date field',
},
}
}
return field
})
: []),
],
},
},
})
```
## Email
This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided.

View File

@@ -309,7 +309,3 @@ import {
...
} from '@payloadcms/plugin-stripe/types';
```
## Examples
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template.

View File

@@ -6,14 +6,14 @@ desc: Converting between lexical richtext and HTML
keywords: lexical, richtext, html
---
## Converting Rich Text to HTML
## Rich Text to HTML
There are two main approaches to convert your Lexical-based rich text to HTML:
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
### Generating HTML on-demand (Recommended)
### On-demand
To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend:
@@ -32,61 +32,81 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
}
```
### Converting Lexical Blocks
#### Dynamic Population (Advanced)
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
```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$>`,
},
})
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 = convertLexicalToHTML({
converters: htmlConverters,
data,
})
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)
}
return <div dangerouslySetInnerHTML={{ __html: html }} />
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Outputting HTML from the Collection
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
To automatically generate HTML from the saved richText field in your Collection, use the `lexicalHTMLField()` helper. This approach converts the JSON to HTML using an `afterRead` hook. For instance:
```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 }} />
}
```
### HTML field
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended for two reasons:
1. It creates a column with duplicate content in another format.
2. In [client-side live preview](/docs/live-preview/client), it makes it not "live".
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
@@ -154,74 +174,59 @@ const Pages: CollectionConfig = {
}
```
### Generating HTML in Your Frontend with Dynamic Population (Advanced)
## Blocks to HTML
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
```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 { 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 }} />
}
```
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
```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 {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
import config from '../../config.js'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
export const MyRSCComponent = async ({
data,
}: {
data: SerializedEditorState
}) => {
const payload = await getPayload({
config,
})
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$>`,
},
})
const html = await convertLexicalToHTMLAsync({
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Converting HTML to Richtext
## HTML to Richtext
If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](/docs/rich-text/converters#retrieving-the-editor-config):

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and JSX
keywords: lexical, richtext, jsx
---
## Converting Richtext to JSX
## Richtext to JSX
To convert richtext to JSX, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the richtext content to it:
@@ -28,7 +28,7 @@ The `RichText` component includes built-in converters for common Lexical nodes.
populated data to work correctly.
</Banner>
### Converting Internal Links
### Internal Links
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.
@@ -81,7 +81,7 @@ export const MyComponent: React.FC<{
}
```
### Converting Lexical Blocks
### Lexical Blocks
If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
@@ -133,9 +133,9 @@ export const MyComponent: React.FC<{
}
```
### Overriding Default JSX Converters
### Overriding 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.
You can override any of the default JSX converters by 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:

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and Markdown / MDX
keywords: lexical, richtext, markdown, md, mdx
---
## Converting Richtext to Markdown
## Richtext to Markdown
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert the lexical editor state to Markdown with the following:
@@ -91,7 +91,7 @@ const Pages: CollectionConfig = {
}
```
## Converting Markdown to Richtext
## Markdown to Richtext
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert Markdown to the lexical editor state with the following:

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and plaintext
keywords: lexical, richtext, plaintext, text
---
## Converting Richtext to Plaintext
## Richtext to Plaintext
Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`.

View File

@@ -142,32 +142,33 @@ import { CallToAction } from '../blocks/CallToAction'
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
| ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`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 |
| **`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 |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
| Feature Name | Included by default | Description |
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`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 |
| **`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 |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
| **`EXPERIMENTAL_TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!

View File

@@ -3,7 +3,7 @@ title: Autosave
label: Autosave
order: 30
desc: Using Payload's Draft functionality, you can configure your collections and globals to autosave changes as drafts, and publish only you're ready.
keywords: version history, revisions, audit log, draft, publish, autosave, Content Management System, cms, headless, javascript, node, react, nextjss
keywords: version history, revisions, audit log, draft, publish, autosave, Content Management System, cms, headless, javascript, node, react, nextjs
---
Extending on Payload's [Draft](/docs/versions/drafts) functionality, you can configure your collections and globals to autosave changes as drafts, and publish only you're ready. The Admin UI will automatically adapt to autosaving progress at an interval that you define, and will store all autosaved changes as a new Draft version. Never lose your work - and publish changes to the live document only when you're ready.

View File

@@ -74,6 +74,7 @@ export const rootEslintConfig = [
'no-console': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
'payload/no-relative-monorepo-imports': 'off',
},
},
]

View File

@@ -1,7 +1,7 @@
import type { CollectionConfig } from 'payload/types'
import { admins } from './access/admins'
import adminsAndUser from './access/adminsAndUser'
import { adminsAndUser } from './access/adminsAndUser'
import { anyone } from './access/anyone'
import { checkRole } from './access/checkRole'
import { loginAfterCreate } from './hooks/loginAfterCreate'
@@ -25,6 +25,7 @@ export const Users: CollectionConfig = {
create: anyone,
update: adminsAndUser,
delete: admins,
unlock: admins,
admin: ({ req: { user } }) => checkRole(['admin'], user),
},
hooks: {

View File

@@ -1,4 +1,4 @@
import type { Access } from 'payload/config'
import type { Access } from 'payload'
import { checkRole } from './checkRole'

View File

@@ -1,19 +1,17 @@
import type { Access } from 'payload/config'
import type { Access } from 'payload'
import { checkRole } from './checkRole'
const adminsAndUser: Access = ({ req: { user } }) => {
export const adminsAndUser: Access = ({ req: { user } }) => {
if (user) {
if (checkRole(['admin'], user)) {
return true
}
return {
id: user.id,
id: { equals: user.id },
}
}
return false
}
export default adminsAndUser

View File

@@ -1,3 +1,3 @@
import type { Access } from 'payload/config'
import type { Access } from 'payload'
export const anyone: Access = () => true

View File

@@ -1,6 +1,6 @@
import type { User } from '../../payload-types'
export const checkRole = (allRoles: User['roles'] = [], user: User = undefined): boolean => {
export const checkRole = (allRoles: User['roles'] = [], user: User | null = null): boolean => {
if (user) {
if (
allRoles.some((role) => {
@@ -8,8 +8,9 @@ export const checkRole = (allRoles: User['roles'] = [], user: User = undefined):
return individualRole === role
})
})
)
{return true}
) {
return true
}
}
return false

View File

@@ -1,4 +1,4 @@
import type { FieldHook } from 'payload/types'
import type { FieldHook } from 'payload'
import type { User } from '../../payload-types'

View File

@@ -1,7 +1,6 @@
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import express from 'express'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'

View File

@@ -1,5 +1,4 @@
import express from 'express'
import type { Request, Response } from 'express'
import { parse } from 'url'
import next from 'next'

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.37.0",
"version": "3.39.1",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.37.0",
"version": "3.39.1",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -1,8 +0,0 @@
import { readFileSync } from 'fs'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const packageJson = JSON.parse(readFileSync(path.resolve(dirname, '../../package.json'), 'utf-8'))
export const PACKAGE_VERSION = packageJson.version

View File

@@ -22,7 +22,9 @@ const updateEnvExampleVariables = (
const [key] = line.split('=')
if (!key) {return}
if (!key) {
return
}
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null

View File

@@ -1,9 +1,8 @@
import type { ProjectTemplate } from '../types.js'
import { error, info } from '../utils/log.js'
import { PACKAGE_VERSION } from './constants.js'
export function validateTemplate(templateName: string): boolean {
export function validateTemplate({ templateName }: { templateName: string }): boolean {
const validTemplates = getValidTemplates()
if (!validTemplates.map((t) => t.name).includes(templateName)) {
error(`'${templateName}' is not a valid template.`)
@@ -20,13 +19,13 @@ export function getValidTemplates(): ProjectTemplate[] {
name: 'blank',
type: 'starter',
description: 'Blank 3.0 Template',
url: `https://github.com/payloadcms/payload/templates/blank#v${PACKAGE_VERSION}`,
url: `https://github.com/payloadcms/payload/templates/blank#main`,
},
{
name: 'website',
type: 'starter',
description: 'Website Template',
url: `https://github.com/payloadcms/payload/templates/website#v${PACKAGE_VERSION}`,
url: `https://github.com/payloadcms/payload/templates/website#main`,
},
{
name: 'plugin',

View File

@@ -1,4 +1,3 @@
import execa from 'execa'
import fse from 'fs-extra'
import { fileURLToPath } from 'node:url'
import path from 'path'
@@ -9,6 +8,7 @@ const dirname = path.dirname(filename)
import type { NextAppDetails } from '../types.js'
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { getLatestPackageVersion } from '../utils/getLatestPackageVersion.js'
import { info } from '../utils/log.js'
import { getPackageManager } from './get-package-manager.js'
import { installPackages } from './install-packages.js'
@@ -36,15 +36,8 @@ export async function updatePayloadInProject(
const packageManager = await getPackageManager({ projectDir })
// Fetch latest Payload version from npm
const { exitCode: getLatestVersionExitCode, stdout: latestPayloadVersion } = await execa('npm', [
'show',
'payload',
'version',
])
if (getLatestVersionExitCode !== 0) {
throw new Error('Failed to fetch latest Payload version')
}
// Fetch latest Payload version
const latestPayloadVersion = await getLatestPackageVersion({ packageName: 'payload' })
if (payloadVersion === latestPayloadVersion) {
return { message: `Payload v${payloadVersion} is already up to date.`, success: true }

View File

@@ -8,7 +8,6 @@ import path from 'path'
import type { CliArgs } from './types.js'
import { configurePayloadConfig } from './lib/configure-payload-config.js'
import { PACKAGE_VERSION } from './lib/constants.js'
import { createProject } from './lib/create-project.js'
import { parseExample } from './lib/examples.js'
import { generateSecret } from './lib/generate-secret.js'
@@ -20,6 +19,7 @@ import { parseTemplate } from './lib/parse-template.js'
import { selectDb } from './lib/select-db.js'
import { getValidTemplates, validateTemplate } from './lib/templates.js'
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
import { getLatestPackageVersion } from './utils/getLatestPackageVersion.js'
import { debug, error, info } from './utils/log.js'
import {
feedbackOutro,
@@ -78,13 +78,18 @@ export class Main {
async init(): Promise<void> {
try {
const debugFlag = this.args['--debug']
const LATEST_VERSION = await getLatestPackageVersion({
debug: debugFlag,
packageName: 'payload',
})
if (this.args['--help']) {
helpMessage()
process.exit(0)
}
const debugFlag = this.args['--debug']
// eslint-disable-next-line no-console
console.log('\n')
p.intro(chalk.bgCyan(chalk.black(' create-payload-app ')))
@@ -200,7 +205,7 @@ export class Main {
const templateArg = this.args['--template']
if (templateArg) {
const valid = validateTemplate(templateArg)
const valid = validateTemplate({ templateName: templateArg })
if (!valid) {
helpMessage()
process.exit(1)
@@ -230,7 +235,7 @@ export class Main {
}
if (debugFlag) {
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${PACKAGE_VERSION}`)
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${LATEST_VERSION}`)
}
if (!exampleArg) {

View File

@@ -0,0 +1,34 @@
/**
* Fetches the latest version of a package from the NPM registry.
*
* Used in determining the latest version of Payload to use in the generated templates.
*/
export async function getLatestPackageVersion({
debug = false,
packageName = 'payload',
}: {
debug?: boolean
/**
* Package name to fetch the latest version for based on the NPM registry URL
*
* Eg. for `'payload'`, it will fetch the version from `https://registry.npmjs.org/payload`
*
* @default 'payload'
*/
packageName?: string
}) {
try {
const response = await fetch(`https://registry.npmjs.org/${packageName}`)
const data = await response.json()
const latestVersion = data['dist-tags'].latest
if (debug) {
console.log(`Found latest version of ${packageName}: ${latestVersion}`)
}
return latestVersion
} catch (error) {
console.error('Error fetching Payload version:', error)
throw error
}
}

View File

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

View File

@@ -372,36 +372,61 @@ const group: FieldSchemaGenerator<GroupField> = (
buildSchemaOptions,
parentIsLocalized,
): void => {
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
if (fieldAffectsData(field)) {
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
// carry indexSortableFields through to versions if drafts enabled
const indexSortableFields =
buildSchemaOptions.indexSortableFields &&
field.name === 'version' &&
buildSchemaOptions.draftsEnabled
// carry indexSortableFields through to versions if drafts enabled
const indexSortableFields =
buildSchemaOptions.indexSortableFields &&
field.name === 'version' &&
buildSchemaOptions.draftsEnabled
const baseSchema: SchemaTypeOptions<any> = {
...formattedBaseSchema,
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
const baseSchema: SchemaTypeOptions<any> = {
...formattedBaseSchema,
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
},
},
},
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
}
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization, parentIsLocalized),
})
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
} else {
field.fields.forEach((subField) => {
if (fieldIsVirtual(subField)) {
return
}
const addFieldSchema = getSchemaGenerator(subField.type)
if (addFieldSchema) {
addFieldSchema(
subField,
schema,
payload,
buildSchemaOptions,
(parentIsLocalized || field.localized) ?? false,
)
}
})
}
}
const json: FieldSchemaGenerator<JSONField> = (

View File

@@ -20,7 +20,6 @@ type SearchParam = {
const subQueryOptions = {
lean: true,
limit: 50,
}
/**
@@ -184,7 +183,7 @@ export async function buildSearchParam({
select[joinPath] = true
}
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
const result = await SubModel.find(subQuery).lean().select(select)
const $in: unknown[] = []

View File

@@ -57,12 +57,8 @@ const relationshipSort = ({
return false
}
for (const [i, segment] of segments.entries()) {
if (versions && i === 0 && segment === 'version') {
segments.shift()
continue
}
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
const field = currentFields.find((each) => each.name === segment)
if (!field) {
@@ -71,6 +67,10 @@ const relationshipSort = ({
if ('fields' in field) {
currentFields = field.flattenedFields
if (field.name === 'version' && versions && i === 0) {
segments.shift()
i--
}
} else if (
(field.type === 'relationship' || field.type === 'upload') &&
i !== segments.length - 1
@@ -106,7 +106,7 @@ const relationshipSort = ({
as: `__${path}`,
foreignField: '_id',
from: foreignCollection.Model.collection.name,
localField: relationshipPath,
localField: versions ? `version.${relationshipPath}` : relationshipPath,
pipeline: [
{
$project: {
@@ -150,6 +150,18 @@ export const buildSortParam = ({
sort = [sort]
}
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
// So we add a fallback sort to ensure that the results are always in the same order.
let fallbackSort = '-id'
if (timestamps) {
fallbackSort = '-createdAt'
}
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
sort.push(fallbackSort)
}
const sorting = sort.reduce<Record<string, string>>((acc, item) => {
let sortProperty: string
let sortDirection: SortDirection

View File

@@ -105,6 +105,7 @@ export const sanitizeQueryValue = ({
| undefined => {
let formattedValue = val
let formattedOperator = operator
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
const segments = path.split('.')
segments.shift()

View File

@@ -151,6 +151,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
query: versionQuery,
session: paginationOptions.options?.session ?? undefined,
sort: paginationOptions.sort as object,
sortAggregation,
useEstimatedCount: paginationOptions.useEstimatedCount,
})
} else {

View File

@@ -128,7 +128,6 @@ const traverseFields = ({
break
}
case 'blocks': {
const blocksSelect = select[field.name] as SelectType

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.37.0",
"version": "3.39.1",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.37.0",
"version": "3.39.1",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.37.0",
"version": "3.39.1",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -4,7 +4,7 @@ import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { buildQuery } from './queries/buildQuery.js'
import { getTransaction } from './utilities/getTransaction.js'
export const count: Count = async function count(

View File

@@ -5,7 +5,7 @@ import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { buildQuery } from './queries/buildQuery.js'
import { getTransaction } from './utilities/getTransaction.js'
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(

View File

@@ -5,7 +5,7 @@ import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { buildQuery } from './queries/buildQuery.js'
import { getTransaction } from './utilities/getTransaction.js'
export const countVersions: CountVersions = async function countVersions(

View File

@@ -6,7 +6,7 @@ import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { buildQuery } from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { transform } from './transform/read/index.js'
import { getTransaction } from './utilities/getTransaction.js'

View File

@@ -4,9 +4,10 @@ import { inArray } from 'drizzle-orm'
import type { DrizzleAdapter } from '../types.js'
import buildQuery from '../queries/buildQuery.js'
import { buildQuery } from '../queries/buildQuery.js'
import { selectDistinct } from '../queries/selectDistinct.js'
import { transform } from '../transform/read/index.js'
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
import { getTransaction } from '../utilities/getTransaction.js'
import { buildFindManyArgs } from './buildFindManyArgs.js'
@@ -75,6 +76,26 @@ export const findMany = async function find({
tableName,
versions,
})
if (orderBy) {
for (const key in selectFields) {
const column = selectFields[key]
if (column.primary) {
continue
}
if (
!orderBy.some(
(col) =>
col.column.name === column.name &&
getNameFromDrizzleTable(col.column.table) === getNameFromDrizzleTable(column.table),
)
) {
delete selectFields[key]
}
}
}
const selectDistinctResult = await selectDistinct({
adapter,
db,

View File

@@ -19,12 +19,17 @@ import toSnakeCase from 'to-snake-case'
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js'
import buildQuery from '../queries/buildQuery.js'
import { buildQuery } from '../queries/buildQuery.js'
import { getTableAlias } from '../queries/getTableAlias.js'
import { operatorMap } from '../queries/operatorMap.js'
import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
import { jsonAggBuildObject } from '../utilities/json.js'
import { rawConstraint } from '../utilities/rawConstraint.js'
import {
InternalBlockTableNameIndex,
resolveBlockTableName,
} from '../utilities/validateExistingBlockIsIdentical.js'
const flattenAllWherePaths = (where: Where, paths: string[]) => {
for (const k in where) {
@@ -196,7 +201,12 @@ export const traverseFields = ({
}
}
const relationName = field.dbName ? `_${arrayTableName}` : `${path}${field.name}`
const relationName = getArrayRelationName({
field,
path: `${path}${field.name}`,
tableName: arrayTableName,
})
currentArgs.with[relationName] = withArray
traverseFields({
@@ -244,7 +254,7 @@ export const traverseFields = ({
;(field.blockReferences ?? field.blocks).forEach((_block) => {
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
const blockKey = `_blocks_${block.slug}`
const blockKey = `_blocks_${block.slug}${!block[InternalBlockTableNameIndex] ? '' : `_${block[InternalBlockTableNameIndex]}`}`
let blockSelect: boolean | SelectType | undefined
@@ -284,8 +294,9 @@ export const traverseFields = ({
with: {},
}
const tableName = adapter.tableNameMap.get(
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
const tableName = resolveBlockTableName(
block,
adapter.tableNameMap.get(`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`),
)
if (typeof blockSelect === 'object') {

View File

@@ -23,7 +23,7 @@ export { migrateFresh } from './migrateFresh.js'
export { migrateRefresh } from './migrateRefresh.js'
export { migrateReset } from './migrateReset.js'
export { migrateStatus } from './migrateStatus.js'
export { default as buildQuery } from './queries/buildQuery.js'
export { buildQuery } from './queries/buildQuery.js'
export { operatorMap } from './queries/operatorMap.js'
export type { Operators } from './queries/operatorMap.js'
export { parseParams } from './queries/parseParams.js'

View File

@@ -28,6 +28,8 @@ export async function migrateReset(this: DrizzleAdapter): Promise<void> {
const req = await createLocalReq({}, payload)
existingMigrations.reverse()
// Rollback all migrations in order
for (const migration of existingMigrations) {
const migrationFile = migrationFiles.find((m) => m.name === migration.name)

View File

@@ -1,4 +1,4 @@
import type { FlattenedBlock, FlattenedField } from 'payload'
import type { FlattenedField } from 'payload'
type Args = {
doc: Record<string, unknown>

View File

@@ -1,49 +1,126 @@
export type Groups =
| 'addColumn'
| 'addConstraint'
| 'alterType'
| 'createIndex'
| 'createTable'
| 'createType'
| 'disableRowSecurity'
| 'dropColumn'
| 'dropConstraint'
| 'dropIndex'
| 'dropTable'
| 'dropType'
| 'notNull'
| 'renameColumn'
| 'setDefault'
/**
* Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement
* example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;
* to: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
* @param sql
* Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement.
* Works with or without a schema name.
*
* Examples:
* 'ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;'
* => 'ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;'
*
* 'ALTER TABLE "public"."pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;'
* => 'ALTER TABLE "public"."pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;'
*/
function convertAddColumnToAlterColumn(sql) {
// Regular expression to match the ADD COLUMN statement with its constraints
const regex = /ALTER TABLE ("[^"]+")\.(".*?") ADD COLUMN ("[^"]+") [\w\s]+ NOT NULL;/
const regex = /ALTER TABLE ((?:"[^"]+"\.)?"[^"]+") ADD COLUMN ("[^"]+") [^;]*?NOT NULL;/i
// Replace the matched part with "ALTER COLUMN ... SET NOT NULL;"
return sql.replace(regex, 'ALTER TABLE $1.$2 ALTER COLUMN $3 SET NOT NULL;')
return sql.replace(regex, 'ALTER TABLE $1 ALTER COLUMN $2 SET NOT NULL;')
}
export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> => {
const groups = {
/**
* example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
*/
addColumn: 'ADD COLUMN',
// example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
/**
* example:
* DO $$ BEGIN
* ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
* EXCEPTION
* WHEN duplicate_object THEN null;
* END $$;
*/
addConstraint: 'ADD CONSTRAINT',
//example:
// DO $$ BEGIN
// ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
// EXCEPTION
// WHEN duplicate_object THEN null;
// END $$;
/**
* example: CREATE TABLE IF NOT EXISTS "payload_locked_documents" (
* "id" serial PRIMARY KEY NOT NULL,
* "global_slug" varchar,
* "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
* "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
* );
*/
createTable: 'CREATE TABLE',
/**
* example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
*/
dropColumn: 'DROP COLUMN',
// example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
/**
* example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
*/
dropConstraint: 'DROP CONSTRAINT',
// example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
/**
* example: DROP TABLE "pages_rels";
*/
dropTable: 'DROP TABLE',
// example: DROP TABLE "pages_rels";
/**
* example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
*/
notNull: 'NOT NULL',
// example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
/**
* example: CREATE TYPE "public"."enum__pages_v_published_locale" AS ENUM('en', 'es');
*/
createType: 'CREATE TYPE',
/**
* example: ALTER TYPE "public"."enum_pages_blocks_cta" ADD VALUE 'copy';
*/
alterType: 'ALTER TYPE',
/**
* example: ALTER TABLE "categories_rels" DISABLE ROW LEVEL SECURITY;
*/
disableRowSecurity: 'DISABLE ROW LEVEL SECURITY;',
/**
* example: DROP INDEX IF EXISTS "pages_title_idx";
*/
dropIndex: 'DROP INDEX IF EXISTS',
/**
* example: ALTER TABLE "pages" ALTER COLUMN "_status" SET DEFAULT 'draft';
*/
setDefault: 'SET DEFAULT',
/**
* example: CREATE INDEX IF NOT EXISTS "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
*/
createIndex: 'INDEX IF NOT EXISTS',
/**
* example: DROP TYPE "public"."enum__pages_v_published_locale";
*/
dropType: 'DROP TYPE',
/**
* columns were renamed from camelCase to snake_case
* example: ALTER TABLE "forms" RENAME COLUMN "confirmationType" TO "confirmation_type";
*/
renameColumn: 'RENAME COLUMN',
}
const result = Object.keys(groups).reduce((result, group: Groups) => {
@@ -51,7 +128,17 @@ export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> =
return result
}, {}) as Record<Groups, string[]>
// push multi-line changes to a single grouping
let isCreateTable = false
for (const line of list) {
if (isCreateTable) {
result.createTable.push(line)
if (line.includes(');')) {
isCreateTable = false
}
continue
}
Object.entries(groups).some(([key, value]) => {
if (line.endsWith('NOT NULL;')) {
// split up the ADD COLUMN and ALTER COLUMN NOT NULL statements
@@ -64,7 +151,11 @@ export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> =
return true
}
if (line.includes(value)) {
result[key].push(line)
let statement = line
if (key === 'dropConstraint') {
statement = line.replace('" DROP CONSTRAINT "', '" DROP CONSTRAINT IF EXISTS "')
}
result[key].push(statement)
return true
}
})

View File

@@ -20,6 +20,17 @@ type Args = {
req?: Partial<PayloadRequest>
}
const runStatementGroup = async ({ adapter, db, debug, statements }) => {
const addColumnsStatement = statements.join('\n')
if (debug) {
adapter.payload.logger.info(debug)
adapter.payload.logger.info(addColumnsStatement)
}
await db.execute(sql.raw(addColumnsStatement))
}
/**
* Moves upload and relationship columns from the join table and into the tables while moving data
* This is done in the following order:
@@ -40,16 +51,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
// get the drizzle migrateUpSQL from drizzle using the last schema
const { generateDrizzleJson, generateMigration, upSnapshot } = adapter.requireDrizzleKit()
const toSnapshot: Record<string, unknown> = {}
for (const key of Object.keys(adapter.schema).filter(
(key) => !key.startsWith('payload_locked_documents'),
)) {
toSnapshot[key] = adapter.schema[key]
}
const drizzleJsonAfter = generateDrizzleJson(toSnapshot) as DrizzleSnapshotJSON
const drizzleJsonAfter = generateDrizzleJson(adapter.schema) as DrizzleSnapshotJSON
// Get the previous migration snapshot
const previousSnapshot = fs
@@ -81,18 +83,62 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const sqlUpStatements = groupUpSQLStatements(generatedSQL)
const addColumnsStatement = sqlUpStatements.addColumn.join('\n')
if (debug) {
payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS')
payload.logger.info(addColumnsStatement)
}
const db = await getTransaction(adapter, req)
await db.execute(sql.raw(addColumnsStatement))
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING TYPES' : null,
statements: sqlUpStatements.createType,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'ALTERING TYPES' : null,
statements: sqlUpStatements.alterType,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING TABLES' : null,
statements: sqlUpStatements.createTable,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'RENAMING COLUMNS' : null,
statements: sqlUpStatements.renameColumn,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING NEW RELATIONSHIP COLUMNS' : null,
statements: sqlUpStatements.addColumn,
})
// SET DEFAULTS
await runStatementGroup({
adapter,
db,
debug: debug ? 'SETTING DEFAULTS' : null,
statements: sqlUpStatements.setDefault,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING INDEXES' : null,
statements: sqlUpStatements.createIndex,
})
for (const collection of payload.config.collections) {
if (collection.slug === 'payload-locked-documents') {
continue
}
const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug))
const pathsToQuery: PathsToQuery = new Set()
@@ -238,52 +284,58 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
}
// ADD CONSTRAINT
const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n')
if (debug) {
payload.logger.info('ADDING CONSTRAINTS')
payload.logger.info(addConstraintsStatement)
}
await db.execute(sql.raw(addConstraintsStatement))
await runStatementGroup({
adapter,
db,
debug: debug ? 'ADDING CONSTRAINTS' : null,
statements: sqlUpStatements.addConstraint,
})
// NOT NULL
const notNullStatements = sqlUpStatements.notNull.join('\n')
if (debug) {
payload.logger.info('NOT NULL CONSTRAINTS')
payload.logger.info(notNullStatements)
}
await db.execute(sql.raw(notNullStatements))
await runStatementGroup({
adapter,
db,
debug: debug ? 'NOT NULL CONSTRAINTS' : null,
statements: sqlUpStatements.notNull,
})
// DROP TABLE
const dropTablesStatement = sqlUpStatements.dropTable.join('\n')
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING TABLES' : null,
statements: sqlUpStatements.dropTable,
})
if (debug) {
payload.logger.info('DROPPING TABLES')
payload.logger.info(dropTablesStatement)
}
await db.execute(sql.raw(dropTablesStatement))
// DROP INDEX
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING INDEXES' : null,
statements: sqlUpStatements.dropIndex,
})
// DROP CONSTRAINT
const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n')
if (debug) {
payload.logger.info('DROPPING CONSTRAINTS')
payload.logger.info(dropConstraintsStatement)
}
await db.execute(sql.raw(dropConstraintsStatement))
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING CONSTRAINTS' : null,
statements: sqlUpStatements.dropConstraint,
})
// DROP COLUMN
const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n')
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING COLUMNS' : null,
statements: sqlUpStatements.dropColumn,
})
if (debug) {
payload.logger.info('DROPPING COLUMNS')
payload.logger.info(dropColumnsStatement)
}
await db.execute(sql.raw(dropColumnsStatement))
// DROP TYPES
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING TYPES' : null,
statements: sqlUpStatements.dropType,
})
}

View File

@@ -56,7 +56,7 @@ export const migrateRelationships = async ({
${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500};
`
paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`))
paginationResult = await db.execute(sql.raw(`${paginationStatement}`))
if (paginationResult.rows.length === 0) {
return
@@ -72,7 +72,7 @@ export const migrateRelationships = async ({
payload.logger.info(statement)
}
const result = await adapter.drizzle.execute(sql.raw(`${statement}`))
const result = await db.execute(sql.raw(`${statement}`))
const docsToResave: DocsToResave = {}

View File

@@ -1,4 +1,4 @@
import type { Table } from 'drizzle-orm'
import type { SQL, Table } from 'drizzle-orm'
import type { FlattenedField, Sort } from 'payload'
import { asc, desc } from 'drizzle-orm'
@@ -16,6 +16,7 @@ type Args = {
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
rawSort?: SQL
selectFields: Record<string, GenericColumn>
sort?: Sort
tableName: string
@@ -31,14 +32,16 @@ export const buildOrderBy = ({
joins,
locale,
parentIsLocalized,
rawSort,
selectFields,
sort,
tableName,
}: Args): BuildQueryResult['orderBy'] => {
const orderBy: BuildQueryResult['orderBy'] = []
const createdAt = adapter.tables[tableName]?.createdAt
if (!sort) {
const createdAt = adapter.tables[tableName]?.createdAt
if (createdAt) {
sort = '-createdAt'
} else {
@@ -50,6 +53,18 @@ export const buildOrderBy = ({
sort = [sort]
}
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
// So we add a fallback sort to ensure that the results are always in the same order.
let fallbackSort = '-id'
if (createdAt) {
fallbackSort = '-createdAt'
}
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
sort.push(fallbackSort)
}
for (const sortItem of sort) {
let sortProperty: string
let sortDirection: 'asc' | 'desc'
@@ -74,17 +89,23 @@ export const buildOrderBy = ({
value: sortProperty,
})
if (sortTable?.[sortTableColumnName]) {
let order = sortDirection === 'asc' ? asc : desc
if (rawSort) {
order = () => rawSort
}
orderBy.push({
column:
aliasTable && tableName === getNameFromDrizzleTable(sortTable)
? aliasTable[sortTableColumnName]
: sortTable[sortTableColumnName],
order: sortDirection === 'asc' ? asc : desc,
order,
})
selectFields[sortTableColumnName] = sortTable[sortTableColumnName]
}
} catch (err) {
} catch (_) {
// continue
}
}

View File

@@ -37,7 +37,8 @@ export type BuildQueryResult = {
selectFields: Record<string, GenericColumn>
where: SQL
}
const buildQuery = function buildQuery({
export const buildQuery = function buildQuery({
adapter,
aliasTable,
fields,
@@ -79,6 +80,7 @@ const buildQuery = function buildQuery({
joins,
locale,
parentIsLocalized,
rawSort: context.rawSort,
selectFields,
sort: context.sort,
tableName,
@@ -91,5 +93,3 @@ const buildQuery = function buildQuery({
where,
}
}
export default buildQuery

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