Compare commits

..

96 Commits

Author SHA1 Message Date
German Jablonski
a44857a2ee cannot reproduce in website template 2025-08-22 15:41:28 +01:00
Alessio Gravili
5c16443431 fix: ensure updates to createdAt and updatedAt are respected (#13335)
Previously, when manually setting `createdAt` or `updatedAt` in a
`payload.db.*` or `payload.*` operation, the value may have been
ignored. In some cases it was _impossible_ to change the `updatedAt`
value, even when using direct db adapter calls. On top of that, this
behavior sometimes differed between db adapters. For example, mongodb
did accept `updatedAt` when calling `payload.db.updateVersion` -
postgres ignored it.

This PR changes this behavior to consistently respect `createdAt` and
`updatedAt` values for `payload.db.*` operations.

For `payload.*` operations, this also works with the following
exception:
- update operations do no respect `updatedAt`, as updates are commonly
performed by spreading the old data, e.g. `payload.update({ data:
{...oldData} })` - in these cases, we usually still want the `updatedAt`
to be updated. If you need to get around this, you can use the
`payload.db.updateOne` operation instead.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210919646303994
2025-08-22 08:41:07 -04:00
Logan Stellway
3408d7bdcd docs: fix small typo in documentation (#13545)
<!--

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?

Fixes a tiny typo in the documentation. 

<!--

### Why?

### How?

Fixes #

-->

Co-authored-by: lstellway <lstellway@users.noreply.github.com>
2025-08-22 10:49:36 +01:00
Alessio Gravili
3bc1d0895f fix(richtext-lexical): toolbar dropdown items are disabled when selecting via double-click (#13544)
Fixes https://github.com/payloadcms/payload/issues/13275 by ensuring
that toolbar styles are updated on mount.

This PR also improves the lexical test suite by adding data attributes
that can be targeted more easily

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211110462564657
2025-08-22 09:44:55 +01:00
Yunsup Sim
ddb8ca4de2 fix(db-*): add delete version id for non-mongodb (#10613)
Fixes #10526 

### What?

`updateVersion` throws error at deleting before version of draft.

### Why?

When triggers `db.updateVersion` with non-mongodb environment,
`saveVersion` in `payload/src/versions/saveVersions.ts` doesn't remove
`id` in `versionData`.

### How?

Add `delete versionData.id` for non-mongodb environments.

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
2025-08-22 09:39:21 +01:00
Vincent Vu
763cb61964 fix(next): update rest route handler types for Next.js 15.5 compatibility (#13521)
Fixes #13527.

When upgrading to [Next.js 15.5](https://nextjs.org/blog/next-15-5) with
Payload, you might experience a runtime or build error similar to this:

```ts
Type error: Type 'typeof import("/src/app/(payload)/api/graphql/route")' does not satisfy the expected type 'RouteHandlerConfig<"/api/graphql">'.
  Types of property 'OPTIONS' are incompatible.
    Type '(request: Request, args: { params: Promise<{ slug: string[]; }>; }) => Promise<Response>' is not assignable to type '(request: NextRequest, context: { params: Promise<{}>; }) => void | Promise<void> | Response | Promise<Response>'.
      Types of parameters 'args' and 'context' are incompatible.
        Type '{ params: Promise<{}>; }' is not assignable to type '{ params: Promise<{ slug: string[]; }>; }'.
          Types of property 'params' are incompatible.
            Type 'Promise<{}>' is not assignable to type 'Promise<{ slug: string[]; }>'.
              Property 'slug' is missing in type '{}' but required in type '{ slug: string[]; }'.
```

This is because Next.js route types are now _stricter_. Our REST handler
is nested within a catch-all `/api/[...slug]` route, so the slug param
_will_ exist in the handler—but the _same_ handler is re-used for the
`/api/graphql` OPTIONS route, which **_is not_** nested within the
`slug` param and so it **_will not_** exist as the types suggest.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211115021865680

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-08-21 16:38:03 -04:00
Elliot DeNolf
60cfc31f65 ci: ignore template bump commits in release notes [skip ci] 2025-08-21 16:12:03 -04:00
Elliot DeNolf
32069e2056 templates: bump for v3.53.0 (#13541)
🤖 Automated bump of templates for v3.53.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 16:02:50 -04:00
Elliot DeNolf
4151a902f2 chore(release): v3.53.0 [skip ci] 2025-08-21 14:27:59 -04:00
Jarrod Flesch
b65ca6832d fix(ui): thread id through instead of from routeParams (#13539)
Adjustment to https://github.com/payloadcms/payload/pull/13526

Prefer to thread ID through arguments instead of relying on routeParams
which would mean that ID is always a string - would not work for PG dbs
or non-text based ID's.
2025-08-21 18:14:58 +00:00
Jarrod Flesch
76741eb722 chore(plugin-multi-tenant): missing collections warning (#13538)
Fixes https://github.com/payloadcms/payload/issues/13517

⚠️ **Need to merge https://github.com/payloadcms/payload/pull/13379
first**

Adds warning if collections are enabled but not found. This can happen
if you add the multi-tenant before other plugins that add the
collections you are attempting to add multi-tenancy to.
2025-08-21 18:01:19 +00:00
Jónas G. Sigurðsson
2bdd669fde feat: add icelandic translations (#13423)
### What?

Add Icelandic translations

### Why?

It hadn't been implemented yet

### How?

I added files, mimicking the existing pattern for translations and
translated all messages in the list
2025-08-21 13:24:20 -04:00
Patrik
96074530b1 fix: server edit view components don't receive document id prop (#13526)
### What?

Make the document `id` available to server-rendered admin components
that expect it—specifically `EditMenuItems` and
`BeforeDocumentControls`—so `props.id` matches the official docs.

### Why?

The docs show examples using `props.id`, but the runtime `serverProps`
and TS types didn’t include it. This led to `undefined` at render time.

### How?

- Add id to ServerProps and set it in renderDocumentSlots from
req.routeParams.id.

Fixes #13420
2025-08-21 13:23:51 -04:00
Jarrod Flesch
5cf215d9cb feat(plugin-multi-tenant): visible tenant field on documents (#13379)
The goal of this PR is to show the selected tenant on the document level
instead of using the global selector to sync the state to the document.


Should merge https://github.com/payloadcms/payload/pull/13316 before
this one.

### Video of what this PR implements

**Would love feedback!**


https://github.com/user-attachments/assets/93ca3d2c-d479-4555-ab38-b77a5a9955e8
2025-08-21 13:15:24 -04:00
Alessio Gravili
393b4a0929 fix(next): no client field found error when accessing version view in some configurations (#13339)
This PR fixes some incorrect field paths handling (=> should not pass
path and schema paths that contain index paths down to sub-fields
outside of the indexPath property) when building the version fields.

Fixes https://github.com/payloadcms/payload/issues/12376

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210932060696925
2025-08-21 10:04:55 -04:00
Dan Ribbens
a94cd95b90 fix: uploads update unnecessarily resizing with sharp (#13528)
fixes https://github.com/payloadcms/payload/issues/13499
2025-08-21 09:24:21 -04:00
Patrik
a04bc9a3e7 fix(translations): stale version.versionCreatedOn key in translation (#13530)
### What?

Remove `versionCreatedOn` from translations.

### Why?

The key is no longer referenced anywhere in the admin/UI code.

Fixes #13188 


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211105332940662
2025-08-20 15:20:40 -04:00
Anders Semb Hermansen
36fd6e905a perf(storage-s3): stream files and abort s3 request from static handler (#13430)
### What?

Stream S3 object directly to response instead of creating a Buffer in
memory and wire up an abort controller to stop streaming if user aborts
download

### Why?

To avoid excessive memory usage and to abort s3 download if user has
aborted the request anyway.

### How?

In node environment the AWS S3 always returns a Readable. The
streamToBuffer method always required this, but the any type hided that
this was actually needed. Now there is an explicit type check, but this
should never trigger in a node server environment.

Wire up and abort controller to the request so that we tell the S3
object to also stop streaming further if the user aborts.

Fixes #10286
Maybe also helps on other issues with s3 and resource usage
2025-08-20 14:53:21 -04:00
Jacob Fletcher
c67ceca8e2 perf(ui): do not fetch doc permissions on autosave (#13477)
No need to re-fetch doc permissions during autosave. This will save us
from making two additional client-side requests on every autosave
interval, on top of the two existing requests needed to autosave and
refresh form state.

This _does_ mean that the UI will not fully reflect permissions again
until you fully save, or until you navigating back, but that has always
been the behavior anyway (until #13416). Maybe we can find another
solution for this in the future, or otherwise consider this to be
expected behavior.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211094073049052
2025-08-20 13:39:35 -04:00
Jacob Fletcher
f382c39dae fix: accept computed array and block rows from server form state (#13501)
If you have a beforeChange hook that manipulates arrays or blocks by
_adding rows_, the result of that hook will not be reflected in the UI
after save or autosave as you might expect.

For example, this hook that ensures at least one array row is populated:

```ts
{
  type: 'array',
  hooks: {
    beforeChange: [
      ({ value }) =>
        !value?.length
          ? [
              // this is an added/computed row if attempt to save with no rows
            ]
          : value,
    ],
  },
  // ...
}
```

When you save without any rows, this hook will have automatically
computed a row for you and saved it to the database. Form state will not
reflect this fact, however, until you refresh or navigate back.

This is for two reasons:
1. When merging server form state, we receive the new fields, but do not
receive the new rows. This is because the `acceptValues` flag only
applies to the `value` property of fields, but should also apply to the
`rows` property on `array` and `blocks` fields too.
2. When creating new form state on the server, the newly added rows are
not being flagged with `addedByServer`, and so never make it into form
state when it is merged in on the client. To do this we need to send the
previous form state to the server and set `renderAllFields` to false in
order receive this property as expected. Fixed by #13524.

Before:


https://github.com/user-attachments/assets/3ab07ef5-3afd-456f-a9a8-737909b75016

After:


https://github.com/user-attachments/assets/27ad1d83-9313-45a9-b44a-db1e64452a99

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211094073049042
2025-08-20 13:39:25 -04:00
Said Akhrarov
fea6742ceb fix(plugin-form-builder): export radio field type (#11908)
<!--

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?
[#11716](https://github.com/payloadcms/payload/pull/11716) introduced
the `RadioField` component. This PR exports the type for use by
end-users.

### Why?
To allow end-users to utilize the type as expected with
`plugin-form-builder`.

### How?
Adds the `RadioField` interface to the list of exported plugin types.

Fixes #11806
2025-08-20 18:27:59 +01:00
Sasha
aa90271a59 fix(db-postgres): camelCase point fields (#13519)
Fixes https://github.com/payloadcms/payload/issues/13394
2025-08-20 20:18:14 +03:00
Jacob Fletcher
5e433aa9c3 perf(ui): reduce number of field renders on submit (#13524)
Needed for #13501.

No need to re-render all fields during save, and especially autosave.
Fields are already rendered. We only need to render new fields that may
have been created by hooks, etc.

We can achieve this by sending previous form state and new data through
the request with `renderAllFields: false`. That way form state can be
built up from previous form state, while still accepting new data.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211094406108904
2025-08-20 12:39:48 -04:00
Gregor Billing
c7b9f0f563 fix(db-mongodb): disable join aggregations in DocumentDB compatibility mode (#13270)
### What?

The PR #12763 added significant improvements for third-party databases
that are compatible with the MongoDB API. While the original PR was
focused on Firestore, other databases like DocumentDB also benefit from
these compatibility features.

In particular, the aggregate JOIN strategy does not work on AWS
DocumentDB and thus needs to be disabled. The current PR aims to provide
this as a sensible default in the `compatibilityOptions` that are
provided by Payload out-of-the-box.

As a bonus, it also fixes a small typo from `compat(a)bility` to
`compat(i)bility`.

### Why?

Because our Payload instance, which is backed by AWS DocumentDB, crashes
upon trying to access any `join` field.

### How?

By adding the existing `useJoinAggregations` with value `false` to the
compatiblity layer. Individual developers can still choose to override
it in their own local config as needed.
2025-08-20 16:31:19 +00:00
Patrik
b3e48f8efa fix(ui): logout type error when user is null during locale switch (#13514)
### What?

Prevent a `TypeError: Cannot read properties of null (reading
'collection')` in the admin UI when switching locales by hardening
`AuthProvider.logOut`.

### Why?

During locale transitions, user can briefly be null. The existing code
used `user.collection` unguarded

### How?

- Use `userSlug` over `user.collection`.
- Always clear local auth in a `finally` block (`setNewUser(null)`,
`revokeTokenAndExpire()`), regardless of request outcome.

Fixes #13313 


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211093549155962
2025-08-20 11:02:09 -04:00
Bob Bass
f44e27691e fix: 'front-end' spelling typo in JSDocs for overrideAccess (#13515)
### What?

Repeated typo in jsdoc documentation has been fixed

### Why?
'front-end' was reading as 'fron-end' in several jsdoc comments
2025-08-20 07:25:40 -07:00
Boyan Bratvanov
a840fc944b docs: fix typo in custom views (#13522)
A very small fix.
2025-08-20 13:40:57 +01:00
German Jablonski
cf427e5519 fix: imports (part 2/2) (#13520)
Completes https://github.com/payloadcms/payload/pull/13513

That PR fixed it for the `admin` suite, but I had also encountered the
same issue in `live-preview`.

When searching for instances, I found others with the same pattern
within packages, which I took the opportunity to fix in case the same
error occurs in the future.
2025-08-20 08:08:55 -04:00
Jacob Fletcher
adb83b1e06 test: add array field helpers (#13493)
Adds various helpers to make it easier and more standard to manage array
fields within e2e tests. Retrofits existing tests to ensure consistent
interactions across the board, and also organizes existing blocks and
relationship field helpers in the same way.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211094073049046
2025-08-19 19:44:59 +00:00
Sasha
368cd901f8 fix(ui): replace deprecated document.execCommand('copy') API with navigator (#13431)
Replaces the deprecated API which might cause issues with the copy
button.

Also adds an E2E test for the copy button
2025-08-19 15:39:31 -04:00
Jarrod Flesch
406a09f4bf fix(ui): local image src changing when title changes (#13442)
When changing the title of a newly uploaded image, the image would
flicker because the `createObjectURL` was running and creating a new
local file url. This change allows `handleFileChange` to run and not
affect the url if the file being added is not a new file.

### Before


https://github.com/user-attachments/assets/9e21101e-c4cc-4fc3-b510-18f1a0d9fb3a



### After


https://github.com/user-attachments/assets/9f310e10-d29c-49a9-bd28-cb6da6c5651a
2025-08-19 15:39:03 -04:00
Patrik
4f6d0d8ed2 fix: select field component value prop type does not support array values (#13510)
### What?

Update `SelectFieldBaseClientProps` type so `value` accepts `string[]`
for `hasMany` selects

### Why?

Multi-selects currently error with “Type 'string[]' is not assignable to
type 'string'”.

### How?

- Change `value?: string` to `value?: string | string[]`
- Also adds additional multi select custom component to `admin` test
suite for testing

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-08-19 11:54:52 -07:00
Jarrod Flesch
9e7bb24ffb test: fix link imports (#13513)
Fixes issue in test suites that import the Link component from
next/link.
2025-08-19 14:51:11 -04:00
Jarrod Flesch
73ba4d1bb9 fix: unable to query versions on latest key (#13512)
Fixes https://github.com/payloadcms/payload/issues/13455

https://github.com/payloadcms/payload/pull/13297 Fixed a scoping issue,
but exposed a new issue where querying versioned documents by the
`latest` key would fail. This PR fixes the newly discoverable issue.
2025-08-19 11:42:02 -07:00
Patrik
332b2a9d3c fix(ui): double ? in gravatar url (#13511)
### What?

Ensure the Gravatar URL appends the query string only once.

### Why?

Previously `src` used `...?${query}` while `query` already began with
`?`, producing `??` and causing the avatar URL to be invalid in some
cases.

### How?

- Keep `query` as `?${params}` (from `URLSearchParams`).
- Change `src` from `https://www.gravatar.com/avatar/${hash}?${query}`
to `https://www.gravatar.com/avatar/${hash}${query}` so only one `?` is
present.

Fixes #13325
2025-08-19 11:03:53 -07:00
Sasha
92d459ec99 fix: avoid re-uploading the file unless changed (#13500)
Fixes https://github.com/payloadcms/payload/issues/13182

Before, the condition in
b714e6b151/packages/payload/src/uploads/generateFileData.ts#
was always passing. Now, with `shouldReupload` we properly check the
difference (whether the image was cropped or the focal point was
changed)
2025-08-19 17:50:50 +03:00
Jan Huenges
7699d02d7f fix(richtext-lexical): use thumbnail component for uploads (#12540)
### What?

Fix the broken thumbnail for non-images for uploads in the
richtext-lexical editor.

**Before:**

![before](https://github.com/user-attachments/assets/dbe5ffb7-9032-435b-8684-3c8bf53ae5bd)

**After:**

![after](https://github.com/user-attachments/assets/1c4af50e-2216-4ada-aff0-7a5e5559ac64)


### Why?

As described in #6867, the thumbnail in the richtext-lexical editor is
always trying to render an thumbnail image. This leads to a broken image
as soon as a non-image is added.

### How?

The fix was done by using the `<Thumbnail />` component from
`@payloadcms/ui`

Fixes #6867

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
2025-08-19 14:14:51 +00:00
Паламар Роман
b714e6b151 fix(templates): plugin template correct paths for exports (#13427)
When using plugin template , initial package.json settings is wrong.
They point to non-existing files in exports folder ( index.ts ,
index.js, index.d.ts ) , which results in broken package if published
(can't import your plugin from package)

Fixes #13426
2025-08-19 14:10:18 +01:00
chenxi-debugger
379ef87d84 docs(db-mongodb): note on indexing localized fields & per-locale growth (#13469)
Closes #13464

Adds a note to the Indexes docs for localized fields:
- Indexing a `localized: true` field creates one index per locale path
(e.g. `slug.en`, `slug.da-dk`), which can grow index count on MongoDB.
- Recommends defining explicit indexes via collection-level `indexes`
for only the locale paths you actually query.
- Includes a concrete example (index `slug.en` only).

Docs-only change.

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
2025-08-19 12:45:38 +00:00
Patrik
9f7d8c65d5 fix(ui): nested fields with admin.disableListColumn still appear as columns in list view (#13504)
### What?

This PR makes `filterFields` recurse into **fields with subfields**
(e.g., tabs, row, group, collapsible, array) so nested fields with
`admin.disableListColumn: true` (or hidden/disabled fields) are properly
excluded.

### Why?

Nested fields with `admin.disableListColumn: true` were still appearing
in the list view.

Example: a text field inside a `row` or `group` continued to show as a
column despite being marked `disableListColumn`.

### How?

- Call `filterFields` recursively for `tab.fields` and for any field
exposing a `fields` array.

Fixes #13496
2025-08-18 11:50:08 -07:00
Patrik
30ea8e1bac fix(ui): blocks field not respecting width styles in row layouts (#13502)
### What?

This PR applies `mergeFieldStyles` to the `BlocksField` component,
ensuring that custom admin styles such as `width` are correctly
respected when Blocks fields are placed inside row layouts.

### Why?

Previously, Blocks fields did not inherit or apply their `admin.width`
(or other merged field styles). For example, when placing two Blocks
fields side by side inside a row with `width: '50%'`, the widths were
ignored, causing layout issues.

### How?

- Imported and used `mergeFieldStyles` within `BlocksField`.
- Applied the merged styles to the root `<div>` via the `style` prop,
consistent with how other field components (like `TextField`) handle
styles.

Fixes #13498
2025-08-18 09:15:40 -07:00
German Jablonski
f9bbca8bfe docs: clarify pagination and improve cross-referencing (#13503)
Fixes #13417

Builds on #13471

---------

Co-authored-by: chenxi-debugger <chenxi.debugger@gmail.com>
2025-08-18 16:55:52 +01:00
id3er0
9d08f503ae fix(storage-s3): validate Content-Length before appending header (#13472)
## Description
Fixes "Parse Error: Invalid character in Content-Length" errors that
occur when S3-compatible storage providers (like MinIO) return undefined
or invalid ContentLength values.

## Changes
- Added validation before appending Content-Length header in
`staticHandler.ts`
- Only appends Content-Length when value is present and numeric
- Prevents HTTP specification violations from undefined/invalid values

## Code Changes
```typescript
const contentLength = String(object.ContentLength);
if (contentLength && !isNaN(Number(contentLength))) {
  headers.append('Content-Length', contentLength);
}
```

## Issue
- Resolves MinIO compatibility issues where undefined ContentLength
causes client parse errors
- Maintains backward compatibility when ContentLength is valid

## Testing
- [x] Tested with MinIO provider returning undefined ContentLength
- [x] Verified valid Content-Length values are still properly set
- [x] Confirmed no regression in existing S3 functionality

### Type of Change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)

### Checklist
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings
- [x] Any dependent changes have been merged and published
```
2025-08-15 19:50:27 +00:00
Patrik
a7ed88b5fa feat(plugin-import-export): use groupBy as SortBy when present and sort is unset (#13491)
### What?

When exporting, if no `sort` parameter is set but a `groupBy` parameter
is present in the list-view query, the export will treat `groupBy` as
the SortBy field and default to ascending order.
Additionally, the SortOrder field in the export UI is now hidden when no
sort is present, reducing visual noise and preventing irrelevant order
selection.

### Why?

Previously, exports ignored `groupBy` entirely when no sort was set,
leading to unsorted output even if the list view was grouped. Also,
SortOrder was always shown, even when no sort field was selected, which
could be confusing. These changes ensure exports reflect the list view’s
grouping and keep the UI focused.

### How?

- Check for `groupBy` in the query only when `sort` is unset.
- If found, set SortBy to `groupBy` and SortOrder to ascending.
- Hide the SortOrder field when `sort` is not set.
- Leave sorting unset if neither `sort` nor `groupBy` are present.
2025-08-15 11:56:58 -07:00
Sasha
ec5b673aca fix: copy to locale with localized fields inside tabs (#13456)
Fixes https://github.com/payloadcms/payload/issues/13374
2025-08-15 14:55:12 -04:00
Sasha
3dd142c637 fix(ui): cannot replace the file if the user does not have delete access (#13484)
Currently, if you don't have delete access to the document, the UI
doesn't allow you to replace the file, which isn't expected. This is
also a UI only restriction, and the API allows you do this fine.

This PR makes so the "remove file" button renders even if you don't have
delete access, while still ensures you have update access.

---------

Co-authored-by: Paul Popus <paul@payloadcms.com>
2025-08-15 14:52:45 -04:00
Jarrod Flesch
1909063e42 fix: omit trashed documents from appearing in folder results (#13492)
### Issue

The folders join field query was returning trashed documents. 

### Fix
Adds a constraint to trash enabled collections, which prevents trashed
documents from being surfaced in folder views.
2025-08-15 14:45:36 -04:00
Anatoly Kopyl
64f4b0aff3 fix: update docker base image in templates (#13020)
<!--

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 #

-->

Went ahead and bumped the base from `node:22.12.0-alpine` to
`node:22.17.0-alpine` across all templates to fix #13019.
2025-08-15 18:49:02 +01:00
Said Akhrarov
c8ef92449b fix(next): add missing translations to version view
<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR translates the block and item label in the version view, as well
as the version label in the step nav.

### Why?
To appropriately translate labels in the version views.

### How?
By using the TFunction from useTranslation as well as existing
translation keys.

Fixes #13193
Fixes #13194
2025-08-15 18:43:01 +01:00
Patrik
b7243b1413 fix(ui): bulk upload action bar buttons wrapping (#13486)
### What?

Added `white-space: nowrap` to the `.bulk-upload--actions-bar__buttons`
class to ensure button labels remain on a single line.

### Why?

In the bulk upload action bar, buttons containing multi-word labels were
wrapping to two lines, causing them to expand vertically and misalign
with other controls.

### How?

Applied `white-space: nowrap` to `.bulk-upload--actions-bar__buttons` so
all button labels stay on one line, maintaining consistent height and
alignment.

#### Before
<img width="1469" height="525" alt="Screenshot 2025-08-15 at 9 20 07 AM"
src="https://github.com/user-attachments/assets/aecc65ae-7b2f-43ba-96c8-1143fcee7f88"
/>

#### After
<img width="1474" height="513" alt="Screenshot 2025-08-15 at 9 19 55 AM"
src="https://github.com/user-attachments/assets/438c6ee1-b966-4966-8686-37ba4619a25c"
/>
2025-08-15 11:49:08 -04:00
Elliot DeNolf
f5d77662b0 templates: bump for v3.52.0 (#13488)
🤖 Automated bump of templates for v3.52.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-15 11:28:20 -04:00
Patrik
efdf00200a feat(plugin-import-export): adds sort order control and sync with sort-by field (#13478)
### What?

This PR adds a dedicated `sortOrder` select field (Ascending /
Descending) to the import-export plugin, alongside updates to the
existing `SortBy` component. The new field and component logic keep the
sort field (`sort`) in sync with the selected sort direction.

### Why?

Previously, descending sorting did not work. While the `SortBy` field
could read the list view’s `query.sort` param, if the value contained a
leading dash (e.g. `-title`), it would not be handled correctly. Only
ascending sorts such as `sort=title` worked, and the preview table would
not reflect a descending order.

### How?

- Added a new `sortOrder` select field to the export options schema.
- Implemented a `SortOrder` custom component using ReactSelect:
- On new exports, reads `query.sort` from the list view and sets both
`sortOrder` and `sort` (combined value with or without a leading dash).
  - Handles external changes to `sort` that include a leading dash.
- Updated `SortBy`:
- No longer writes to `sort` during initial hydration—`SortOrder` owns
initial value setting.
- On user field changes, writes the combined value using the current
`sortOrder`.
2025-08-15 11:27:54 -04:00
Elliot DeNolf
217606ac20 chore(release): v3.52.0 [skip ci] 2025-08-15 10:42:32 -04:00
jacobsfletch
0b60bf2eff fix(ui): significantly more predictable autosave form state (#13460) 2025-08-14 19:36:02 -04:00
Sasha
46699ec314 test: skip cookies filter for internal URLs in getExternalFile (#13476)
Test for https://github.com/payloadcms/payload/pull/13475
2025-08-14 13:17:17 -04:00
Sean Zubrickas
cdd90f91c8 docs: updates image paths to new screenshots (#13461)
Updates images paths for the following screenshots:

- auth-overview.jpg
- autosave-drafts.jpg
- autosave-v3.jpg
- uploads-overview.jpg
- versions-v3.jpg
2025-08-14 13:15:35 -04:00
Sasha
8d4e7f5f30 fix: filter payload- cookies in getExternalFile only if the URL is external (#13475)
Fixes a regression from
https://github.com/payloadcms/payload/pull/13215. Fixes the issue when
`skipSafeFetch: true` is set
https://github.com/payloadcms/payload/issues/13146#issuecomment-3066858749

This PR makes it so we still send the cookies if we do `fetch` to our
server, but filter them when we `fetch` to an external server (usually a
third party storage, for which we don't want to expose those cookies)
2025-08-14 14:51:24 +00:00
jacobsfletch
b426052cab test: fix import-export plugin int (#13474)
CI is blocked because of failing int tests within the import/export
plugin suite after #13380.
2025-08-14 08:54:13 -04:00
Sasha
047519f47f fix(db-postgres): ensure index names are not too long (#13428)
Fixes https://github.com/payloadcms/payload/issues/13196
2025-08-14 02:44:56 +03:00
Dan Ribbens
c1c68fbb55 feat(plugin-import-export): adds limit and page fields to export options (#13380)
### What:
This PR adds `limit` and `page` fields to the export options, allowing
users to control the number of documents exported and the page from
which to start the export. It also enforces that limit must be a
positive multiple of 100.

### Why:
This feature is needed to provide pagination support for large exports,
enabling users to export manageable chunks of data rather than the
entire dataset at once. Enforcing multiples-of-100 for `limit` ensures
consistent chunking behavior and prevents unexpected export issues.

### How:
- The `limit` field determines the maximum number of documents to export
and **must be a positive multiple of 100**.
- The `page` field defines the starting page of the export and is
displayed only when a `limit` is specified.
- If `limit` is cleared, the `page` resets to 1 to maintain consistency.
- Export logic was adjusted to respect the `limit` and `page` values
when fetching documents.

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
2025-08-13 14:01:45 -07:00
Patrik
3e65111bc1 fix(plugin-import-export): csv export & preview showing full documents for hasMany monomorphic relationships instead of just ID (#13465)
### What?

Fixes an issue where CSV exports and the preview table displayed all
fields of documents in hasMany monomorphic relationships instead of only
their IDs.

### Why?

This caused cluttered output and inconsistent CSV formats, since only
IDs should be exported for hasMany monomorphic relationships.

### How?

Added explicit `toCSV` handling for all relationship types in
`getCustomFieldFunctions`, updated `flattenObject` to delegate to these
handlers, and adjusted `getFlattenedFieldKeys` to generate the correct
headers.
2025-08-13 13:54:32 -07:00
Elliot DeNolf
0e8a6c0162 templates: bump for v3.51.0 (#13457)
🤖 Automated bump of templates for v3.51.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-13 09:47:04 -04:00
Elliot DeNolf
0688050eb6 chore(release): v3.51.0 [skip ci] 2025-08-13 09:20:13 -04:00
Jessica Rynkar
5a99d8c5f4 fix: upload with no filename gives vague error (#13414)
### What?
Adds validation to the file upload field to ensure a filename is
provided. If the filename is missing, a clear error message is shown to
the user instead of a general error.

### Why?
Currently, attempting to upload a file without a filename results in a
generic error message: `Something went wrong.` This makes it unclear for
users to understand what the issue is.

### How?
The upload field validation has been updated to explicitly check for a
missing filename. If the filename is undefined or null, the error
message `A filename is required` is now shown.

Fixes #13410
2025-08-13 12:26:59 +01:00
Jessica Rynkar
35ca98e70e fix: version view breaks when tab field has function for label (#13415)
### What?

Fixes an issue where using a function as the `label` for a `tabs` field
causes the versions UI to break.

### Why?

The versions UI was not properly resolving function labels on `tab`
fields, leading to a crash when trying to render them.

### How?

Tweaked the logic so that if the label is a function, it gets called
before rendering.

Fixes #13375
2025-08-13 09:02:43 +01:00
Jacob Fletcher
255bba9606 feat(ui): update query presets ux (#13095)
Surfaces query preset controls more prominently. Query presets are
central to the function of the list view, if enabled, but the UI is
easily overlooked. This also sets the stage for future enhancements,
such as pinned presets, etc.

Also improves the usability of the search field by extending the hitbox
of the input fully to the boundaries of the container.

Before:


https://github.com/user-attachments/assets/3203561c-68cc-43f4-8ded-c51b7c8e8f0c

After:


https://github.com/user-attachments/assets/13dce7c9-67d8-471f-a85c-2795938b3e3e

---

- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210743577153864

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-08-12 18:59:06 +00:00
Jacob Fletcher
8173180d1d fix(ui): autosave form state discards local changes (#13438)
Follow-up to #13416. Supersedes #13434.

When autosave is triggered and the user continues to modify fields,
their changes are overridden by the server's value, i.e. the value at
the time the form state request was made. This makes it almost
impossible to edit fields when using a small autosave interval and/or a
slow network.

This is because autosave is now merged into form state, which by default
uses `acceptValues: true`. This does exactly what it sounds like,
accepts all the values from the server—which may be stale if underlying
changes have been made. We ignore these values for onChange events,
because the user is actively making changes. But during form
submissions, we can accept them because the form is disabled while
processing anyway.

This pattern allows us to render "computed values" from the server, i.e.
a field with an `beforeChange` hook that modifies its value.

Autosave, on the other hand, happens in the background _while the form
is still active_. This means changes may have been made since sending
the request. We still need to accept computed values from the server,
but we need to avoid doing this if the user has active changes since the
time of the request.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211027929253429
2025-08-12 14:28:04 -04:00
Patrik
3258e78596 test: group-by reset and navigation tests in trash view (#13401)
### What?
Adds e2e tests that verify group-by functionality within the trash view
of a collection.

### Why?
To ensure group-by behaves correctly when viewing soft-deleted
documents, including:
- Clearing the group-by selection via the reset button.
- Navigating from grouped rows to the trashed document's edit view.

### How?
- Added `should properly clear group-by in trash view` to test the reset
button behavior.
- Added `should properly navigate to trashed doc edit view from group-by
in trash view` to confirm correct linking and routing.
2025-08-12 09:49:47 -07:00
Alessio Gravili
ad2564e5fa fix: ensure scheduling by default only handles default queue, add allQueues config to autoRun (#13395)
By default, `payload.jobs.run` only runs jobs from the `default` queue
(since https://github.com/payloadcms/payload/pull/12799). It exposes an
`allQueues` property to run jobs from all queues.

For handling schedules (`payload.jobs.handleSchedules` and
`config.jobs.autoRun`), this behaves differently - jobs are run from all
queues by default, and no `allQueues` property exists.

This PR adds an `allQueues` property to scheduling, as well as changes
the default behavior to only handle schedules for the `default` queue.
That way, the behavior of running and scheduling jobs matches.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210982048221260
2025-08-12 11:55:17 -04:00
Jarrod Flesch
995f96bc70 feat(plugin-multi-tenant): allow tenant field overrides (#13316)
Allows user to override more of the tenant field config. Now you can
override most of the field config with:

### At the root level
```ts
/**
 * Field configuration for the field added to all tenant enabled collections
 */
tenantField?: RootTenantFieldConfigOverrides
```

### At the collection level
Setting collection level overrides will replace the root level overrides
shown above.

```ts
collections: {
  [key in CollectionSlug]?: {
    // ... rest of the types
    /**
     * Overrides for the tenant field, will override the entire tenantField configuration
     */
    tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
  }
}
```
2025-08-12 11:33:29 -04:00
Paul
306b7f6943 fix(ui): misalignment of nested toggles on document API tab (#13424)
Fixes the weird misalignment of toggles in the API tab


Before:
<img width="799" height="1011" alt="image"
src="https://github.com/user-attachments/assets/5fc9768c-24de-4d89-a1ba-6dd606f76bec"
/>


After:
<img width="815" height="1069" alt="image"
src="https://github.com/user-attachments/assets/253dfdaa-7fca-4d16-b61e-a91474b9d6c9"
/>
2025-08-12 11:28:35 -04:00
Marcus Michaels
72f5763c25 fix(ui): field elements showing over the top of Preview content (#13148) 2025-08-12 11:15:10 -04:00
Luke Bennett
a374aabd8d fix(ui): bulk upload issues (#13413)
### What?
This PR contains a couple of fixes to the bulk upload process:
- Credentials not being passed when fetching, leading to auth issues
- Provide a fallback to crypto.randomUUID which is only available when
using HTTPS or localhost

### Why?
I use [separate admin and API URLs](#12682) and work off a remote dev
server using custom hostnames. These issues may not impact the happy
path of using localhost, but are dealbreakers in this environment.

### Fixes #
_These are issues I found myself, I fixed them rather than raising
issues for somebody else to pick up, but I can create issues to link to
if required._
2025-08-12 11:10:01 -04:00
Jacob Fletcher
2bc9a2def4 fix(templates): only generate slug from title on demand (#12956)
Currently, the slug field is generated from the title field
indefinitely, even after the document has been created and the initial
slug has been assigned. This should only occur on create, however, as it
currently does, or when the user explicitly requests it.

Given that slugs often determine the URL structure of the webpage that
the document corresponds to, they should rarely change after being
published, and when they do, would require HTTP redirects, etc. to do
right in a production environment.

But this is also a problem with Live Preview which relies on a constant
iframe src. If your Live Preview URL includes the slug as a route param,
which is often the case, then changing the slug will result in a broken
connection as the queried document can no longer be found. The current
workaround is to save the document and refresh the page.

Now, the slug is only generated on initial create, or when the user
explicitly clicks the new "generate" button above the slug field. In the
future we can evaluate supporting dynamic Live Preview URLs.

Regenerating this URL on every change would put additional load on the
client as it would have to reestablish connection every time it changes,
but it should be supported still. See #13055.

Discord discussion here:
https://discord.com/channels/967097582721572934/1102950643259424828/1387737976892686346

Related: #10536
2025-08-11 17:12:43 -04:00
Jacob Fletcher
1d81b0c6dd fix(ui): autosave hooks are not reflected in form state (#13416)
Fixes #10515. Needed for #12956.

Hooks run within autosave are not reflected in form state.

Similar to #10268, but for autosave events.

For example, if you are using a computed value, like this:

```ts
[
  // ...
  {
    name: 'title',
    type: 'text',
  },
  {
    name: 'computedTitle',
    type: 'text',
    hooks: {
      beforeChange: [({ data }) => data?.title],
    },
  },
]
```

In the example above, when an autosave event is triggered after changing
the `title` field, we expect the `computedTitle` field to match. But
although this takes place on the database level, the UI does not reflect
this change unless you refresh the page or navigate back and forth.

Here's an example:

Before:


https://github.com/user-attachments/assets/c8c68a78-9957-45a8-a710-84d954d15bcc

After:


https://github.com/user-attachments/assets/16cb87a5-83ca-4891-b01f-f5c4b0a34362

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210561273449855
2025-08-11 16:59:03 -04:00
German Jablonski
9c8f3202e4 fix(richtext-lexical, plugin-multi-tenant): text editor exposes documents from other tenants (#13229)
## What

Before this PR, an internal link in the Lexical editor could reference a
document from a different tenant than the active one.

Reproduction:
1. `pnpm dev plugin-multi-tenant`
2. Log in with `dev@payloadcms.com` and password `test`
3. Go to `http://localhost:3000/admin/collections/food-items` and switch
between the `Blue Dog` and `Steel Cat` tenants to see which food items
each tenant has.
4. Go to http://localhost:3000/admin/collections/food-items/create, and
in the new richtext field enter an internal link
5. In the relationship select menu, you will see the 6 food items at
once (3 of each of those tenants). In the relationship select menu, you
would previously see all 6 food items at once (3 from each of those
tenants). Now, you'll only see the 3 from the active tenant.

The new test verifies that this is fixed.

## How

`baseListFilter` is used, but now it's called `baseFilter` for obvious
reasons: it doesn't just filter the List View. Having two different
properties where the same function was supposed to be placed wasn't
feasible. `baseListFilter` is still supported for backwards
compatibility. It's used as a fallback if `baseFilter` isn't defined,
and it's documented as deprecated.

`baseFilter` is injected into `filterOptions` of the internal link field
in the Lexical Editor.
2025-08-07 11:24:15 -04:00
Rodrigo Antunes
161769e50c chore(templates): fix typo in e2e tests (headging -> heading) (#13391)
### What?

Small typo in templates e2e tests: `headging`
2025-08-07 15:10:45 +00:00
Sasha
c9a1590fc4 fix(ui): search in select fields with filterOptions (#13397)
Fixes https://github.com/payloadcms/payload/issues/13236
2025-08-07 09:57:56 -04:00
Alessio Gravili
e870be094e docs: add dependency troubleshooting docs (#13385)
Dependency mismanagement is the #1 cause of issues people have with
payload. This PR adds a details docs section explaining why those issues
occur and how to fix them.

**[👀 Click here for docs
preview](https://payloadcms.com/docs/dynamic/troubleshooting/troubleshooting?branch=docs/dependencies)**
2025-08-06 11:19:32 -04:00
Jacob Fletcher
d4f198651c fix(next): group by boolean values (#13382)
When grouping by a checkbox field, boolean values are not translated,
causing labels to render incorrectly, and falsey values to render
without a heading.


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210979856538211
2025-08-05 20:07:29 -04:00
Yi Chi
5d8f8dc0a5 fix(translations): update traditional chinese (zh-TW) localization (#13370)
A comprehensive revision has been made to correct the majority of
localization translation errors. Previous versions were often
grammatically incorrect and awkward. This update includes a one-time
overhaul to improve grammar, vocabulary, and fix a few terms that were
written in Simplified Chinese.

Since a large number of translated terms have been corrected, it is
recommended to use GitHub's Files changed feature to review the diffs
directly.

This Pull Request only modifies translation content; no other code
changes have been made.


<!--

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 #

-->
2025-08-05 20:12:58 +00:00
Jacob Fletcher
7344d64be3 fix(next): group by dates with null values (#13381)
When grouping by a date field and its value is null, the list view
crashes.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210979856538208
2025-08-05 15:54:55 -04:00
Sam
2211f3dd1c fix: use thumbnailUrl for upload documents in folder view (#13368)
### What?
Fix the folder view for upload documents only using
`formatFolderOrDocumentItem()` function and only if the upload is an
image, even when there's a `thumbnailURL` available.

### Why?
Folder view for upload collections (especially those with sharp resizing
disabled) renders different thumbnails between the folder view and list
view. With sharp resizing disabled and an `adminThumbnail` fn provided,
the list view will correctly render optimised images, while the folder
view renders full source images - resulting in a huge discrepancy in
loaded image sizes.

### How?
We're passing the `value.thumbnailURL` **before** the
`formatFolderOrDocumentItem()` call rather than passing it directly as a
function parameter to cover cases where non-image uploads have a
`thumbnailURL` defined.

Fixes #13246
2025-08-05 13:39:59 -04:00
Elliot DeNolf
ac40185158 ci: update release-commenter permissions (#13373)
Permissions for release commenter needed to be explicit.

Docs from upstream:
https://github.com/apexskier/github-release-commenter/pull/549
2025-08-05 10:00:42 -04:00
Elliot DeNolf
d622d3c5e7 chore(release): v3.50.0 [skip ci] 2025-08-05 09:33:56 -04:00
Jessica Rynkar
b74f4fb9b2 fix(ui): fallback to default locale checkbox passes wrong value (#12396)
### What?
Allows document to successfully be saved when `fallback to default
locale` checked without throwing an error.

### Why?
The `fallback to default locale` checkbox allows users to successfully
save a document in the admin panel while using fallback data for
required fields, this has been broken since the release of `v3`.

Without the checkbox override, the user would be prevented from saving
the document in the UI because the field is required and will throw an
error.

The logic of using fallback data is not affected by this checkbox - it
is purely to allow saving the document in the UI.

### How?
The `fallback` checkbox used to have an `onChange` function that
replaces the field value with null, allowing it to get processed through
the standard localization logic and get replaced by fallback data.
However, this `onChange` was removed at some point and the field was
passing the actual checkbox value `true`/`false` which then breaks the
form and prevent it from saving.

This fallback checkbox is only displayed when `fallback: true` is set in
the localization config.
This PR also updated the checkbox to only be displayed when `required:
true` - when it's the field is not `required` this checkbox serves no
purpose.

Also adds tests to `localization/e2e`.

Fixes #11245

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-08-05 09:29:10 -04:00
Patrik
8401b2166d fix: deletedAt in TypeWithID not accepting null from generated types (#13363)
### What?

Updated `TypeWithID` so `deletedAt` can accept `null`.

### Why?

Generated collection types for trash use:

```
deletedAt?: string | null
```

`TypeWithID` previously only allowed `string | undefined`, so assigning
documents with `deletedAt: null` caused TypeScript errors.

Aligning the types fixes this mismatch and ensures `TypeWithID` is
compatible with the generated types.

### How?

Modified the `TypeWithID` definition to:

```
export type TypeWithID = {
  deletedAt?: string | null
  docId?: any
  id: number | string
}
```

This makes `deletedAt` effectively `string | null | undefined`, matching
generated types and eliminating type errors.

Fixes #13341
2025-08-05 09:01:01 -04:00
Jarrod Flesch
20b4de94ee fix(plugin-multi-tenant): constrain results to assigned tenants when present (#13365)
Extension of https://github.com/payloadcms/payload/pull/13213

This PR correctly filters tenants, users and documents based on the
users assigned tenants if any are set. If a user is assigned tenants
then list results should only show documents with those tenants (when
selector is not set). Previously you could construct access results that
allows them to see them, but in the confines of the admin panel they
should not see them. If you wanted a user to be able to see a "public"
tenant while inside the admin panel they either need to be added to the
tenant or have no tenants at all.

Note that this is for filtering only, access control still controls what
documents a user has _access_ to a document. The filters are and always
have been a way to filter out results in the list view.
2025-08-05 09:00:36 -04:00
Jarrod Flesch
43b4b22af9 fix: svg uploads allowed when glob (#13356)
Builds on top of https://github.com/payloadcms/payload/pull/13276 and
allows images/* to also parse and accept svg files correctly.
2025-08-04 23:47:37 -04:00
Jarrod Flesch
3b9dba8641 chore: adds session helper exports (#13367)
Creates/exports `addSessionToUser` helper and also exports the
`removeExpiredSessions` helper.
2025-08-04 22:53:36 -04:00
Alessio Gravili
1d70d4d36c fix(next): version view did not properly handle field-level permissions (#13336)
Field-level permissions were not handled correctly at all. If you had a
field set with access control, this would mean that nested fields would
incorrectly be omitted from the version view.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210932060696919
2025-08-02 12:01:48 -07:00
Jens Becker
1b31c74d32 perf(ui): only select the useAsTitle field when fetching the document data of a relationship value (#13228)
### What?
Optimize the relationship value loading by selecting only the
`useAsTitle` field when fetching document data via the REST API.

### Why?
Previously, all fields were fetched via a POST request when loading the
document data of a relationship value, causing unnecessary data transfer
and slower performance. Only the `useAsTitle` field is needed to display
the related document’s title in the relationship UI field.

### How?
Applied a select to the REST API POST request, similar to how the
options list is loaded, limiting the response to the `useAsTitle` field
only.
2025-08-01 16:18:21 -04:00
Sasha
f432cc1956 feat(graphql): allow to pass count: true to a join query (#13351)
Fixes https://github.com/payloadcms/payload/issues/13077
2025-08-01 18:05:54 +03:00
Jarrod Flesch
2903486974 fix(ui): group/array error paths persisting when valid (#13347)
Fields such as groups and arrays would not always reset errorPaths when
there were no more errors. The server and client state was not being
merged safely and the client state was always persisting when the server
sent back no errorPaths, i.e. itterable fields with fully valid
children. This change ensures errorPaths is defaulted to an empty array
if it is not present on the incoming field.

Likely a regression from
https://github.com/payloadcms/payload/pull/9388.

Adds e2e test.
2025-08-01 16:04:51 +01:00
Jarrod Flesch
b965db881e fix(ui): add hidden sidebar fields css specificity (#13344)
Adds specificity to sidebar no fields css styling.
2025-07-31 08:48:38 -04:00
Jarrod Flesch
1b93c4becc fix(ui): hide sidebar when no fields rendered (#13340) 2025-07-30 23:59:46 -04:00
545 changed files with 8798 additions and 3115 deletions

View File

@@ -17,6 +17,9 @@ env:
jobs:
post_release:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:

2
.gitignore vendored
View File

@@ -331,5 +331,7 @@ test/databaseAdapter.js
test/.localstack
test/google-cloud-storage
test/azurestoragedata/
/media-without-delete-access
licenses.csv

View File

@@ -739,7 +739,7 @@ The `useDocumentInfo` hook provides information about the current document being
| **`lastUpdateTime`** | Timestamp of the last update to the document. |
| **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. |
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). |
| **`savedDocumentData`** | The saved data of the document. |
| **`data`** | The saved data of the document. |
| **`setDocFieldPreferences`** | Method to set preferences for a specific field. [More details](./preferences). |
| **`setDocumentTitle`** | Method to set the document title. |
| **`setHasPublishedDoc`** | Method to update whether the document has been published. |

View File

@@ -33,7 +33,7 @@ export const Users: CollectionConfig = {
}
```
![Authentication Admin Panel functionality](https://payloadcms.com/images/docs/auth-admin.jpg)
![Authentication Admin Panel functionality](https://payloadcms.com/images/docs/auth-overview.jpg)
_Admin Panel screenshot depicting an Admins Collection with Auth enabled_
## Config Options

View File

@@ -141,8 +141,8 @@ The following options are available:
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
| `pagination` | Set pagination-specific options for this Collection in the List View. [More details](#pagination). |
| `baseFilter` | Defines a default base filter which will be applied to the List View (along with any other filters applied by the user) and internal links in Lexical Editor, |
<Banner type="warning">
**Note:** If you set `useAsTitle` to a relationship or join field, it will use

View File

@@ -158,7 +158,7 @@ export function MyCustomView(props: AdminViewServerProps) {
<Banner type="success">
**Tip:** For consistent layout and navigation, you may want to wrap your
Custom View with one of the built-in [Template](./overview#templates).
Custom View with one of the built-in [Templates](./overview#templates).
</Banner>
### View Templates

View File

@@ -293,7 +293,6 @@ Here's an example of a custom `editMenuItems` component:
```tsx
import React from 'react'
import { PopupList } from '@payloadcms/ui'
import type { EditMenuItemsServerProps } from 'payload'
@@ -301,12 +300,12 @@ export const EditMenuItems = async (props: EditMenuItemsServerProps) => {
const href = `/custom-action?id=${props.id}`
return (
<PopupList.ButtonGroup>
<PopupList.Button href={href}>Custom Edit Menu Item</PopupList.Button>
<PopupList.Button href={href}>
<>
<a href={href}>Custom Edit Menu Item</a>
<a href={href}>
Another Custom Edit Menu Item - add as many as you need!
</PopupList.Button>
</PopupList.ButtonGroup>
</a>
</>
)
}
```

View File

@@ -63,3 +63,22 @@ export const MyCollection: CollectionConfig = {
],
}
```
## Localized fields and MongoDB indexes
When you set `index: true` or `unique: true` on a localized field, MongoDB creates one index **per locale path** (e.g., `slug.en`, `slug.da-dk`, etc.). With many locales and indexed fields, this can quickly approach MongoDB's per-collection index limit.
If you know you'll query specifically by a locale, index only those locale paths using the collection-level `indexes` option instead of setting `index: true` on the localized field. This approach gives you more control and helps avoid unnecessary indexes.
```ts
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
fields: [{ name: 'slug', type: 'text', localized: true }],
indexes: [
// Index English slug only (rather than all locales)
{ fields: ['slug.en'] },
// You could also make it unique:
// { fields: ['slug.en'], unique: true },
],
}
```

View File

@@ -60,21 +60,21 @@ You can access Mongoose models as follows:
## Using other MongoDB implementations
You can import the `compatabilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
You can import the `compatibilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
```ts
import { mongooseAdapter, compatabilityOptions } from '@payloadcms/db-mongodb'
import { mongooseAdapter, compatibilityOptions } from '@payloadcms/db-mongodb'
export default buildConfig({
db: mongooseAdapter({
url: process.env.DATABASE_URI,
// For example, if you're using firestore:
...compatabilityOptions.firestore,
...compatibilityOptions.firestore,
}),
})
```
We export compatability options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
We export compatibility options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
- Azure Cosmos DB does not support transactions that update two or more documents in different collections, which is a common case when using Payload (via hooks).
- Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.

View File

@@ -296,11 +296,16 @@ query {
sort: "createdAt"
limit: 5
where: { author: { equals: "66e3431a3f23e684075aaeb9" } }
"""
Optionally pass count: true if you want to retrieve totalDocs
"""
count: true -- s
) {
docs {
title
}
hasNextPage
totalDocs
}
}
}

View File

@@ -162,6 +162,11 @@ const result = await payload.find({
})
```
<Banner type="info">
`pagination`, `page`, and `limit` are three related properties [documented
here](/docs/queries/pagination).
</Banner>
### Find by ID#collection-find-by-id
```js

View File

@@ -207,7 +207,7 @@ Everything mentioned above applies to local development as well, but there are a
### Enable Turbopack
<Banner type="warning">
**Note:** In the future this will be the default. Use as your own risk.
**Note:** In the future this will be the default. Use at your own risk.
</Banner>
Add `--turbo` to your dev script to significantly speed up your local development server start time.

View File

@@ -54,8 +54,15 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* After a tenant is deleted, the plugin will attempt
* to clean up related documents
* Base path for your application
*
* https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath
*
* @default undefined
*/
basePath?: string
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
@@ -68,22 +75,36 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to
* behave as a global
* Set to `true` if you want the collection to behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Overrides for the tenant field, will override the entire tenantField configuration
*/
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
/**
* Set to `false` if you want to manually apply
* the baseListFilter
* the baseFilter
*
* @default true
*/
useBaseFilter?: boolean
/**
* @deprecated Use `useBaseFilter` instead. If both are defined,
* `useBaseFilter` will take precedence. This property remains only
* for backward compatibility and may be removed in a future version.
*
* Originally, `baseListFilter` was intended to filter only the List View
* in the admin panel. However, base filtering is often required in other areas
* such as internal link relationships in the Lexical editor.
*
* @default true
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access
* manually without the multi-tenant constraints applied
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
*
* @default true
*/
@@ -92,8 +113,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
}
/**
* Enables debug mode
* - Makes the tenant field visible in the
* admin UI within applicable collections
* - Makes the tenant field visible in the admin UI within applicable collections
*
* @default false
*/
@@ -105,27 +125,41 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
enabled?: boolean
/**
* Field configuration for the field added
* to all tenant enabled collections
* Localization for the plugin
*/
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant
* enabled collections
*
* @default 'tenant'
*/
name?: string
i18n?: {
translations: {
[key in AcceptedLanguages]?: {
/**
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>'
*/
'confirm-modal-tenant-switch--body'?: string
/**
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
*
* @default 'Confirm {{tenantLabel}} change'
*/
'confirm-modal-tenant-switch--heading'?: string
/**
* @default 'Assigned Tenant'
*/
'field-assignedTenant-label'?: string
/**
* @default 'Tenant'
*/
'nav-tenantSelector-label'?: string
}
}
}
/**
* Field configuration for the field added
* to the users collection
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: RootTenantFieldConfigOverrides
/**
* Field configuration for the field added to the users collection
*
* If `includeDefaultField` is `false`, you must
* include the field on your users collection manually
* This is useful if you want to customize the field
* or place the field in a specific location
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
*/
tenantsArrayField?:
| {
@@ -146,8 +180,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `true`, the field will
* be added to the users collection automatically
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
*/
includeDefaultField?: true
/**
@@ -164,8 +197,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
arrayFieldName?: string
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `false`, you must
* include the field on your users collection manually
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
@@ -174,8 +206,9 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* Customize tenant selector label
*
* Either a string or an object where the keys are i18n
* codes and the values are the string labels
* Either a string or an object where the keys are i18n codes and the values are the string labels
*
* @deprecated Use `i18n.translations` instead.
*/
tenantSelectorLabel?:
| Partial<{
@@ -189,27 +222,25 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
tenantsSlug?: string
/**
* Function that determines if a user has access
* to _all_ tenants
* Function that determines if a user has access to _all_ tenants
*
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
user: ConfigTypes extends { user: unknown }
? ConfigTypes['user']
: TypedUser,
) => boolean
/**
* Opt out of adding access constraints to
* the tenants collection
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter
* tenants by selected tenant
* Opt out including the baseListFilter to filter tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter
* users by selected tenant
* Opt out including the baseListFilter to filter users by selected tenant
*/
useUsersTenantFilter?: boolean
}

View File

@@ -6,9 +6,112 @@ desc: Troubleshooting Common Issues in Payload
keywords: admin, components, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs, troubleshooting
---
## Common Issues
## Dependency mismatches
### "Unauthorized, you must be logged in to make this request" when attempting to log in
All `payload` and `@payloadcms/*` packages must be on exactly the same version and installed only once.
When two copies—or two different versions—of any of these packages (or of `react` / `react-dom`) appear in your dependency graph, you can see puzzling runtime errors. The most frequent is a broken React context:
```bash
TypeError: Cannot destructure property 'config' of...
```
This happens because one package imports a hook (most commonly `useConfig`) from _version A_ while the context provider comes from _version B_. The fix is always the same: make sure every Payload-related and React package resolves to the same module.
### Confirm whether duplicates exist
The first thing to do is to confirm whether duplicative dependencies do in fact exist.
There are two ways to do this:
1. Using pnpm's built-in inspection tool
```bash
pnpm why @payloadcms/ui
```
This prints the dependency tree and shows which versions are being installed. If you see more than one distinct version—or the same version listed under different paths—you have duplication.
2. Manual check (works with any package manager)
```bash
find node_modules -name package.json \
-exec grep -H '"name": "@payloadcms/ui"' {} \;
```
Most of these hits are likely symlinks created by pnpm. Edit the matching package.json files (temporarily add a comment or change a description) to confirm whether they point to the same physical folder or to multiple copies.
Perform the same two checks for react and react-dom; a second copy of React can cause identical symptoms.
#### If no duplicates are found
`@payloadcms/ui` intentionally contains two bundles of itself, so you may see dual paths even when everything is correct. Inside the Payload Admin UI you must import only:
- `@payloadcms/ui`
- `@payloadcms/ui/rsc`
- `@payloadcms/ui/shared`
Any other deep import such as `@payloadcms/ui/elements/Button` should **only** be used in your own frontend, outside of the Payload Admin Panel. Those deep entries are published un-bundled to help you tree-shake and ship a smaller client bundle if you only need a few components from `@payloadcms/ui`.
### Fixing depedendency issues
These steps assume `pnpm`, which the Payload team recommends and uses internally. The principles apply to other package managers like npm and yarn as well. Do note that yarn 1.x is not supported by Payload.
1. Pin every critical package to an exact version
In package.json remove `^` or `~` from all versions of:
- `payload`
- `@payloadcms/*`
- `react`
- `react-dom`
Prefixes allow your package manager to float to a newer minor/patch release, causing mismatches.
2. Delete node_modules
Old packages often linger even after you change versions or removed them from your package.json. Deleting node_modules ensures a clean slate.
3. Re-install dependencies
```bash
pnpm install
```
#### If the error persists
1. Clean the global store (pnpm only)
```bash
pnpm store prune
```
2. Delete the lockfile
Depending on your package manager, this could be `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`.
Make sure you delete the lockfile **and** the node_modules folder at the same time, then run `pnpm install`. This forces a fresh, consistent resolution for all packages. It will also update all packages with dynamic versions to the latest version.
While it's best practice to manage dependencies in such a way where the lockfile can easily be re-generated (often this is the easiest way to resolve dependency issues), this may break your project if you have not tested the latest versions of your dependencies.
If you are using a version control system, make sure to commit your lockfile after this step.
3. Deduplicate anything that slipped through
```bash
pnpm dedupe
```
**Still stuck?**
- Switch to `pnpm` if you are on npm. Its symlinked store helps reducing accidental duplication.
- Inspect the lockfile directly for peer-dependency violations.
- Check project-level .npmrc / .pnpmfile.cjs overrides.
- Run [Syncpack](https://www.npmjs.com/package/syncpack) to enforce identical versions of every `@payloadcms/*`, `react`, and `react-dom` reference.
Absolute last resort: add Webpack aliases so that all imports of a given package resolve to the same path (e.g. `resolve.alias['react'] = path.resolve('./node_modules/react')`). Keep this only until you can fix the underlying version skew.
## "Unauthorized, you must be logged in to make this request" when attempting to log in
This means that your auth cookie is not being set or accepted correctly upon logging in. To resolve check the following settings in your Payload Config:

View File

@@ -13,8 +13,8 @@ keywords: uploads, images, media, overview, documentation, Content Management Sy
</Banner>
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/upload-admin.jpg"
srcDark="https://payloadcms.com/images/docs/upload-admin.jpg"
srcLight="https://payloadcms.com/images/docs/uploads-overview.jpg"
srcDark="https://payloadcms.com/images/docs/uploads-overview.jpg"
alt="Shows an Upload enabled collection in the Payload Admin Panel"
caption="Admin Panel screenshot depicting a Media Collection with Upload enabled"
/>

View File

@@ -12,7 +12,7 @@ Extending on Payload's [Draft](/docs/versions/drafts) functionality, you can con
Autosave relies on Versions and Drafts being enabled in order to function.
</Banner>
![Autosave Enabled](/images/docs/autosave-enabled.png)
![Autosave Enabled](/images/docs/autosave-v3.jpg)
_If Autosave is enabled, drafts will be created automatically as the document is modified and the Admin UI adds an indicator describing when the document was last saved to the top right of the sidebar._
## Options

View File

@@ -14,7 +14,7 @@ Payload's Draft functionality builds on top of the Versions functionality to all
By enabling Versions with Drafts, your collections and globals can maintain _newer_, and _unpublished_ versions of your documents. It's perfect for cases where you might want to work on a document, update it and save your progress, but not necessarily make it publicly published right away. Drafts are extremely helpful when building preview implementations.
![Drafts Enabled](/images/docs/drafts-enabled.png)
![Drafts Enabled](/images/docs/autosave-drafts.jpg)
_If Drafts are enabled, the typical Save button is replaced with new actions which allow you to either save a draft, or publish your changes._
## Options

View File

@@ -13,7 +13,7 @@ keywords: version history, revisions, audit log, draft, publish, restore, autosa
When enabled, Payload will automatically scaffold a new Collection in your database to store versions of your document(s) over time, and the Admin UI will be extended with additional views that allow you to browse document versions, view diffs in order to see exactly what has changed in your documents (and when they changed), and restore documents back to prior versions easily.
![Versions](/images/docs/versions.png)
![Versions](/images/docs/versions-v3.jpg)
_Comparing an old version to a newer version of a document_
**With Versions, you can:**

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.49.1",
"version": "3.53.0",
"private": true,
"type": "module",
"workspaces": [

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.49.1",
"version": "3.53.0",
"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.49.1",
"version": "3.53.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

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

View File

@@ -17,10 +17,16 @@ export const create: Create = async function create(
const options: CreateOptions = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let doc
if (!data.createdAt) {
data.createdAt = new Date().toISOString()
}
transform({
adapter: this,
data,

View File

@@ -14,6 +14,10 @@ export const createGlobal: CreateGlobal = async function createGlobal(
) {
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug })
if (!data.createdAt) {
;(data as any).createdAt = new Date().toISOString()
}
transform({
adapter: this,
data,
@@ -24,6 +28,8 @@ export const createGlobal: CreateGlobal = async function createGlobal(
const options: CreateOptions = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let [result] = (await Model.create([data], options)) as any

View File

@@ -25,6 +25,8 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
const options = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const data = {
@@ -37,6 +39,9 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
updatedAt,
version: versionData,
}
if (!data.createdAt) {
data.createdAt = new Date().toISOString()
}
const fields = buildVersionGlobalFields(this.payload.config, globalConfig)

View File

@@ -29,6 +29,8 @@ export const createVersion: CreateVersion = async function createVersion(
const options = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const data = {
@@ -41,6 +43,9 @@ export const createVersion: CreateVersion = async function createVersion(
updatedAt,
version: versionData,
}
if (!data.createdAt) {
data.createdAt = new Date().toISOString()
}
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)

View File

@@ -331,7 +331,7 @@ export function mongooseAdapter({
}
}
export { compatabilityOptions } from './utilities/compatabilityOptions.js'
export { compatibilityOptions } from './utilities/compatibilityOptions.js'
/**
* Attempt to find migrations directory.

View File

@@ -63,7 +63,10 @@ const migrateModelWithBatching = async ({
},
},
})),
{ session },
{
session, // Timestamps are manually added by the write transform
timestamps: false,
},
)
skip += batchSize

View File

@@ -26,6 +26,8 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
transform({ adapter: this, data, fields, globalSlug, operation: 'write' })

View File

@@ -39,6 +39,8 @@ export async function updateGlobalVersion<T extends TypeWithID>(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const query = await buildQuery({

View File

@@ -36,6 +36,8 @@ export const updateJobs: UpdateJobs = async function updateMany(
lean: true,
new: true,
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let query = await buildQuery({

View File

@@ -58,6 +58,8 @@ export const updateMany: UpdateMany = async function updateMany(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let query = await buildQuery({

View File

@@ -38,6 +38,8 @@ export const updateOne: UpdateOne = async function updateOne(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const query = await buildQuery({

View File

@@ -45,6 +45,8 @@ export const updateVersion: UpdateVersion = async function updateVersion(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const query = await buildQuery({

View File

@@ -2,9 +2,9 @@ import type { Args } from '../index.js'
/**
* Each key is a mongo-compatible database and the value
* is the recommended `mongooseAdapter` settings for compatability.
* is the recommended `mongooseAdapter` settings for compatibility.
*/
export const compatabilityOptions = {
export const compatibilityOptions = {
cosmosdb: {
transactionOptions: false,
useJoinAggregations: false,
@@ -12,6 +12,7 @@ export const compatabilityOptions = {
},
documentdb: {
disableIndexHints: true,
useJoinAggregations: false,
},
firestore: {
disableIndexHints: true,

View File

@@ -395,6 +395,10 @@ describe('transform', () => {
data,
fields: config.collections[0].fields,
})
if ('updatedAt' in data) {
delete data.updatedAt
}
const flattenValuesAfter = Object.values(flattenRelationshipValues(data))
flattenValuesAfter.forEach((value, i) => {

View File

@@ -548,4 +548,10 @@ export const transform = ({
parentIsLocalized,
ref: data,
})
if (operation === 'write') {
if (!data.updatedAt) {
data.updatedAt = new Date().toISOString()
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.49.1",
"version": "3.53.0",
"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.49.1",
"version": "3.53.0",
"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.49.1",
"version": "3.53.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -791,9 +791,14 @@ export const traverseFields = ({
} else {
shouldSelect = true
}
const tableName = fieldShouldBeLocalized({ field, parentIsLocalized })
? `${currentTableName}${adapter.localesSuffix}`
: currentTableName
if (shouldSelect) {
args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name)
args.extras[name] = sql
.raw(`ST_AsGeoJSON("${adapter.tables[tableName][name].name}")::jsonb`)
.as(name)
}
break
}

View File

@@ -546,6 +546,19 @@ export const traverseFields = ({
valuesToTransform.forEach(({ localeKey, ref, value }) => {
let formattedValue = value
if (field.type === 'date') {
if (fieldName === 'updatedAt' && !formattedValue) {
// let the db handle this
formattedValue = new Date().toISOString()
} else {
if (typeof value === 'number' && !Number.isNaN(value)) {
formattedValue = new Date(value).toISOString()
} else if (value instanceof Date) {
formattedValue = value.toISOString()
}
}
}
if (typeof value !== 'undefined') {
if (value && field.type === 'point' && adapter.name !== 'sqlite') {
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
@@ -570,19 +583,6 @@ export const traverseFields = ({
formattedValue = sql.raw(`${columnName} + ${value.$inc}`)
}
if (field.type === 'date') {
if (typeof value === 'number' && !Number.isNaN(value)) {
formattedValue = new Date(value).toISOString()
} else if (value instanceof Date) {
formattedValue = value.toISOString()
}
}
}
if (field.type === 'date' && fieldName === 'updatedAt') {
// let the db handle this
formattedValue = new Date().toISOString()
}
if (typeof formattedValue !== 'undefined') {

View File

@@ -42,6 +42,10 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
upsertTarget,
where,
}: Args): Promise<T> => {
if (operation === 'create' && !data.createdAt) {
data.createdAt = new Date().toISOString()
}
let insertedRow: Record<string, unknown> = { id }
if (id && shouldUseOptimizedUpsertRow({ data, fields })) {
const { row } = transformForWrite({

View File

@@ -9,7 +9,12 @@ export const buildIndexName = ({
name: string
number?: number
}): string => {
const indexName = `${name}${number ? `_${number}` : ''}_idx`
let indexName = `${name}${number ? `_${number}` : ''}_idx`
if (indexName.length > 60) {
const suffix = `${number ? `_${number}` : ''}_idx`
indexName = `${name.slice(0, 60 - suffix.length)}${suffix}`
}
if (!adapter.indexes.has(indexName)) {
adapter.indexes.add(indexName)

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.49.1",
"version": "3.53.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -379,9 +379,11 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
),
},
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
totalDocs: { type: GraphQLInt },
},
}),
args: {
count: { type: GraphQLBoolean },
limit: {
type: GraphQLInt,
},
@@ -402,7 +404,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
},
async resolve(parent, args, context: Context) {
const { collection } = field
const { limit, page, sort, where } = args
const { count = false, limit, page, sort, where } = args
const { req } = context
const draft = Boolean(args.draft ?? context.req.query?.draft)
@@ -429,7 +431,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
throw new Error('GraphQL with array of join.field.collection is not implemented')
}
const { docs } = await req.payload.find({
const { docs, totalDocs } = await req.payload.find({
collection,
depth: 0,
draft,
@@ -439,7 +441,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
locale: req.locale,
overrideAccess: false,
page,
pagination: false,
pagination: count ? true : false,
req,
sort,
where: fullWhere,
@@ -454,6 +456,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
return {
docs: shouldSlice ? docs.slice(0, -1) : docs,
hasNextPage: limit === 0 ? false : limit < docs.length,
...(count ? { totalDocs } : {}),
}
},
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.49.1",
"version": "3.53.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.49.1",
"version": "3.53.0",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.49.1",
"version": "3.53.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -9,7 +9,7 @@ const handlerBuilder =
async (
request: Request,
args: {
params: Promise<{ slug: string[] }>
params: Promise<{ slug?: string[] }>
},
): Promise<Response> => {
const awaitedConfig = await config

View File

@@ -1,15 +1,17 @@
@import '~@payloadcms/ui/scss';
$tab-width: 16px;
$tab-width: 24px;
@layer payload-default {
.query-inspector {
--tab-width: 24px;
&__json-children {
position: relative;
&--nested {
& li {
padding-left: $tab-width;
padding-left: 8px;
}
}
@@ -23,6 +25,14 @@ $tab-width: 16px;
}
}
&__row-line {
&--nested {
.query-inspector__json-children {
padding-left: var(--tab-width);
}
}
}
&__list-wrap {
position: relative;
}
@@ -37,10 +47,16 @@ $tab-width: 16px;
border-bottom-right-radius: 0;
position: relative;
display: flex;
gap: 10px;
column-gap: 14px;
row-gap: 10px;
align-items: center;
left: -3px;
left: 0;
width: calc(100% + 3px);
background-color: var(--theme-elevation-50);
&:not(.query-inspector__list-toggle--empty) {
margin-left: calc(var(--tab-width) * -1 - 10px);
}
svg .stroke {
stroke: var(--theme-elevation-400);
@@ -82,14 +98,32 @@ $tab-width: 16px;
&__bracket {
position: relative;
&--nested {
margin-left: $tab-width;
}
&--position-end {
left: 1px;
left: 2px;
width: calc(100% - 5px);
}
}
// Some specific rules targetting the very top of the nested JSON structure or very first items since they need slightly different styling
&__results {
& > .query-inspector__row-line--nested {
& > .query-inspector__list-toggle {
margin-left: 0;
column-gap: 6px;
.query-inspector__toggle-row-icon {
margin-left: -4px;
}
}
& > .query-inspector__json-children {
padding-left: calc(var(--base) * 1);
}
& > .query-inspector__bracket--nested > .query-inspector__bracket--position-end {
padding-left: 16px;
}
}
}
}
}

View File

@@ -333,6 +333,7 @@ export const renderDocument = async ({
}
const documentSlots = renderDocumentSlots({
id,
collectionConfig,
globalConfig,
hasSavePermission,

View File

@@ -1,6 +1,5 @@
import type {
BeforeDocumentControlsServerPropsOnly,
DefaultServerFunctionArgs,
DocumentSlots,
EditMenuItemsServerPropsOnly,
PayloadRequest,
@@ -27,10 +26,11 @@ export const renderDocumentSlots: (args: {
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
hasSavePermission: boolean
id?: number | string
permissions: SanitizedDocumentPermissions
req: PayloadRequest
}) => DocumentSlots = (args) => {
const { collectionConfig, globalConfig, hasSavePermission, req } = args
const { id, collectionConfig, globalConfig, hasSavePermission, req } = args
const components: DocumentSlots = {} as DocumentSlots
@@ -39,6 +39,7 @@ export const renderDocumentSlots: (args: {
const isPreviewEnabled = collectionConfig?.admin?.preview || globalConfig?.admin?.preview
const serverProps: ServerProps = {
id,
i18n: req.i18n,
payload: req.payload,
user: req.user,
@@ -169,10 +170,11 @@ export const renderDocumentSlots: (args: {
return components
}
export const renderDocumentSlotsHandler: ServerFunction<{ collectionSlug: string }> = async (
args,
) => {
const { collectionSlug, req } = args
export const renderDocumentSlotsHandler: ServerFunction<{
collectionSlug: string
id?: number | string
}> = async (args) => {
const { id, collectionSlug, req } = args
const collectionConfig = req.payload.collections[collectionSlug]?.config
@@ -187,6 +189,7 @@ export const renderDocumentSlotsHandler: ServerFunction<{ collectionSlug: string
})
return renderDocumentSlots({
id,
collectionConfig,
hasSavePermission,
permissions: docPermissions,

View File

@@ -143,7 +143,7 @@ export const handleGroupBy = async ({
},
})
let heading = valueOrRelationshipID || req.i18n.t('general:noValue')
let heading = valueOrRelationshipID
if (
groupByField?.type === 'relationship' &&
@@ -155,14 +155,24 @@ export const handleGroupBy = async ({
valueOrRelationshipID
}
if (groupByField.type === 'date') {
if (groupByField.type === 'date' && valueOrRelationshipID) {
heading = formatDate({
date: String(heading),
date: String(valueOrRelationshipID),
i18n: req.i18n,
pattern: clientConfig.admin.dateFormat,
})
}
if (groupByField.type === 'checkbox') {
if (valueOrRelationshipID === true) {
heading = req.i18n.t('general:true')
}
if (valueOrRelationshipID === false) {
heading = req.i18n.t('general:false')
}
}
if (groupData.docs && groupData.docs.length > 0) {
const { columnState: newColumnState, Table: NewTable } = renderTable({
clientCollectionConfig,
@@ -174,7 +184,7 @@ export const handleGroupBy = async ({
enableRowSelections,
groupByFieldPath,
groupByValue: valueOrRelationshipID,
heading,
heading: heading || req.i18n.t('general:noValue'),
i18n: req.i18n,
key: `table-${valueOrRelationshipID}`,
orderableFieldName: collectionConfig.orderable === true ? '_order' : undefined,

View File

@@ -138,16 +138,14 @@ export const renderListView = async (
throw new Error('not-found')
}
let baseListFilter = undefined
if (typeof collectionConfig.admin?.baseListFilter === 'function') {
baseListFilter = await collectionConfig.admin.baseListFilter({
limit: query.limit,
page: query.page,
req,
sort: query.sort,
})
}
const baseFilterConstraint = await (
collectionConfig.admin?.baseFilter ?? collectionConfig.admin?.baseListFilter
)?.({
limit: query.limit,
page: query.page,
req,
sort: query.sort,
})
let queryPreset: QueryPreset | undefined
let queryPresetPermissions: SanitizedCollectionPermission | undefined
@@ -155,7 +153,7 @@ export const renderListView = async (
let whereWithMergedSearch = mergeListSearchAndWhere({
collectionConfig,
search: typeof query?.search === 'string' ? query.search : undefined,
where: combineWhereConstraints([query?.where, baseListFilter]),
where: combineWhereConstraints([query?.where, baseFilterConstraint]),
})
if (trash === true) {

View File

@@ -26,9 +26,11 @@ export const LogoutClient: React.FC<{
const { startRouteTransition } = useRouteTransition()
const [isLoggedOut, setIsLoggedOut] = React.useState<boolean>(!user)
const isLoggedIn = React.useMemo(() => {
return Boolean(user?.id)
}, [user?.id])
const logOutSuccessRef = React.useRef(false)
const navigatingToLoginRef = React.useRef(false)
const [loginRoute] = React.useState(() =>
formatAdminURL({
@@ -45,26 +47,26 @@ export const LogoutClient: React.FC<{
const router = useRouter()
const handleLogOut = React.useCallback(async () => {
const loggedOut = await logOut()
setIsLoggedOut(loggedOut)
await logOut()
if (!inactivity && loggedOut && !logOutSuccessRef.current) {
if (!inactivity && !navigatingToLoginRef.current) {
toast.success(t('authentication:loggedOutSuccessfully'))
logOutSuccessRef.current = true
navigatingToLoginRef.current = true
startRouteTransition(() => router.push(loginRoute))
return
}
}, [inactivity, logOut, loginRoute, router, startRouteTransition, t])
useEffect(() => {
if (!isLoggedOut) {
if (isLoggedIn) {
void handleLogOut()
} else {
} else if (!navigatingToLoginRef.current) {
navigatingToLoginRef.current = true
startRouteTransition(() => router.push(loginRoute))
}
}, [handleLogOut, isLoggedOut, loginRoute, router, startRouteTransition])
}, [handleLogOut, isLoggedIn, loginRoute, router, startRouteTransition])
if (isLoggedOut && inactivity) {
if (!isLoggedIn && inactivity) {
return (
<div className={`${baseClass}__wrap`}>
<h2>{t('authentication:loggedOutInactivity')}</h2>

View File

@@ -90,7 +90,7 @@ export const SetStepNav: React.FC<{
}),
},
{
label: 'Versions',
label: t('version:versions'),
url: formatAdminURL({
adminRoute,
path: `${docBasePath}/versions`,
@@ -118,7 +118,7 @@ export const SetStepNav: React.FC<{
}),
},
{
label: 'Versions',
label: t('version:versions'),
url: formatAdminURL({
adminRoute,
path: `/globals/${globalSlug}/versions`,

View File

@@ -17,10 +17,16 @@ import {
type SanitizedFieldPermissions,
type VersionField,
} from 'payload'
import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared'
import {
fieldIsID,
fieldShouldBeLocalized,
getFieldPaths,
getFieldPermissions,
getUniqueListBy,
tabHasName,
} from 'payload/shared'
import { diffComponents } from './fields/index.js'
import { getFieldPathsModified } from './utilities/getFieldPathsModified.js'
export type BuildVersionFieldsArgs = {
clientSchemaMap: ClientFieldSchemaMap
@@ -84,7 +90,7 @@ export const buildVersionFields = ({
continue
}
const { indexPath, path, schemaPath } = getFieldPathsModified({
const { indexPath, path, schemaPath } = getFieldPaths({
field,
index: fieldIndex,
parentIndexPath,
@@ -223,21 +229,16 @@ const buildVersionField = ({
BuildVersionFieldsArgs,
'fields' | 'parentIndexPath' | 'versionFromSiblingData' | 'versionToSiblingData'
>): BaseVersionField | null => {
const fieldName: null | string = 'name' in field ? field.name : null
const { permissions, read: hasReadPermission } = getFieldPermissions({
field,
operation: 'read',
parentName: parentPath?.includes('.')
? parentPath.split('.')[parentPath.split('.').length - 1]
: parentPath,
permissions: fieldPermissions,
})
const hasPermission =
fieldPermissions === true ||
!fieldName ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.read
const subFieldPermissions =
fieldPermissions === true ||
!fieldName ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.fields
if (!hasPermission) {
if (!hasReadPermission) {
return null
}
@@ -285,34 +286,54 @@ const buildVersionField = ({
indexPath: tabIndexPath,
path: tabPath,
schemaPath: tabSchemaPath,
} = getFieldPathsModified({
} = getFieldPaths({
field: tabAsField,
index: tabIndex,
parentIndexPath: indexPath,
parentPath,
parentSchemaPath,
})
let tabPermissions: typeof fieldPermissions = undefined
if (typeof permissions === 'boolean') {
tabPermissions = permissions
} else if (permissions && typeof permissions === 'object') {
if ('name' in tab) {
tabPermissions =
typeof permissions.fields?.[tab.name] === 'object'
? permissions.fields?.[tab.name].fields
: permissions.fields?.[tab.name]
} else {
tabPermissions = permissions.fields
}
}
const tabVersion = {
name: 'name' in tab ? tab.name : null,
fields: buildVersionFields({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions,
fieldPermissions: tabPermissions,
fields: tab.fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: isNamedTab ? '' : tabIndexPath,
parentIsLocalized: parentIsLocalized || tab.localized,
parentPath: isNamedTab ? tabPath : path,
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
parentPath: isNamedTab ? tabPath : 'name' in field ? path : parentPath,
parentSchemaPath: isNamedTab
? tabSchemaPath
: 'name' in field
? schemaPath
: parentSchemaPath,
req,
selectedLocales,
versionFromSiblingData: 'name' in tab ? valueFrom?.[tab.name] : valueFrom,
versionToSiblingData: 'name' in tab ? valueTo?.[tab.name] : valueTo,
}).versionFields,
label: tab.label,
label: typeof tab.label === 'function' ? tab.label({ i18n, t: i18n.t }) : tab.label,
}
if (tabVersion?.fields?.length) {
baseVersionField.tabs.push(tabVersion)
@@ -324,6 +345,13 @@ const buildVersionField = ({
}
} // At this point, we are dealing with a `row`, `collapsible`, etc
else if ('fields' in field) {
let subfieldPermissions: typeof fieldPermissions = undefined
if (typeof permissions === 'boolean') {
subfieldPermissions = permissions
} else if (permissions && typeof permissions === 'object') {
subfieldPermissions = permissions.fields
}
if (field.type === 'array' && (valueTo || valueFrom)) {
const maxLength = Math.max(
Array.isArray(valueTo) ? valueTo.length : 0,
@@ -339,15 +367,15 @@ const buildVersionField = ({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions,
fieldPermissions: subfieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + i,
parentSchemaPath: schemaPath,
parentPath: ('name' in field ? path : parentPath) + '.' + i,
parentSchemaPath: 'name' in field ? schemaPath : parentSchemaPath,
req,
selectedLocales,
versionFromSiblingData: fromRow,
@@ -363,7 +391,7 @@ const buildVersionField = ({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions,
fieldPermissions: subfieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
@@ -421,19 +449,32 @@ const buildVersionField = ({
}
}
let blockPermissions: typeof fieldPermissions = undefined
if (permissions === true) {
blockPermissions = true
} else {
const permissionsBlockSpecific = permissions?.blocks?.[blockSlugToMatch]
if (permissionsBlockSpecific === true) {
blockPermissions = true
} else {
blockPermissions = permissionsBlockSpecific?.fields
}
}
baseVersionField.rows[i] = buildVersionFields({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions,
fieldPermissions: blockPermissions,
fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath: path + '.' + i,
parentSchemaPath: schemaPath + '.' + toBlock.slug,
parentPath: ('name' in field ? path : parentPath) + '.' + i,
parentSchemaPath: ('name' in field ? schemaPath : parentSchemaPath) + '.' + toBlock.slug,
req,
selectedLocales,
versionFromSiblingData: fromRow,
@@ -459,7 +500,7 @@ const buildVersionField = ({
*/
diffMethod: 'diffWordsWithSpace',
field: clientField,
fieldPermissions: subFieldPermissions,
fieldPermissions: typeof permissions === 'object' ? permissions.fields : permissions,
parentIsLocalized,
nestingLevel: nestingLevel ? nestingLevel : undefined,

View File

@@ -25,7 +25,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
parentIsLocalized,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
const { i18n, t } = useTranslation()
const { selectedLocales } = useSelectedLocales()
const { config } = useConfig()
@@ -73,7 +73,9 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
})
const rowNumber = String(i + 1).padStart(2, '0')
const rowLabel = fieldIsArrayType(field) ? `Item ${rowNumber}` : `Block ${rowNumber}`
const rowLabel = fieldIsArrayType(field)
? `${t('general:item')} ${rowNumber}`
: `${t('fields:block')} ${rowNumber}`
return (
<div className={`${baseClass}__row`} key={i}>

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.49.1",
"version": "3.53.0",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.49.1",
"version": "3.53.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -25,7 +25,7 @@ type SelectFieldBaseClientProps = {
readonly onChange?: (e: string | string[]) => void
readonly path: string
readonly validate?: SelectFieldValidation
readonly value?: string
readonly value?: string | string[]
}
type SelectFieldBaseServerProps = Pick<FieldPaths, 'path'>

View File

@@ -56,6 +56,12 @@ export type FieldState = {
fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue?: unknown
/**
* @experimental - Note: this property is experimental and may change in the future. Use at your own discretion.
* Every time a field is changed locally, this flag is set to true. Prevents form state from server from overwriting local changes.
* After merging server form state, this flag is reset.
*/
isModified?: boolean
/**
* The path of the field when its custom components were last rendered.
* This is used to denote if a field has been rendered, and if so,
@@ -114,9 +120,11 @@ export type BuildFormStateArgs = {
mockRSCs?: boolean
operation?: 'create' | 'update'
readOnly?: boolean
/*
If true, will render field components within their state object
*/
/**
* If true, will render field components within their state object.
* Performance optimization: Setting to `false` ensures that only fields that have changed paths will re-render, e.g. new array rows, etc.
* For example, you only need to render ALL fields on initial render, not on every onChange.
*/
renderAllFields?: boolean
req: PayloadRequest
returnLockStatus?: boolean

View File

@@ -1,5 +1,3 @@
import { v4 as uuid } from 'uuid'
import type {
AuthOperationsFromCollectionSlug,
Collection,
@@ -24,7 +22,7 @@ import { getFieldsToSign } from '../getFieldsToSign.js'
import { getLoginOptions } from '../getLoginOptions.js'
import { isUserLocked } from '../isUserLocked.js'
import { jwtSign } from '../jwt.js'
import { removeExpiredSessions } from '../removeExpiredSessions.js'
import { addSessionToUser } from '../sessions.js'
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts.js'
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js'
@@ -285,34 +283,15 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
user,
}
if (collectionConfig.auth.useSessions) {
// Add session to user
const newSessionID = uuid()
const now = new Date()
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
const expiresAt = new Date(now.getTime() + tokenExpInMs)
const { sid } = await addSessionToUser({
collectionConfig,
payload,
req,
user,
})
const session = { id: newSessionID, createdAt: now, expiresAt }
if (!user.sessions?.length) {
user.sessions = [session]
} else {
user.sessions = removeExpiredSessions(user.sessions)
user.sessions.push(session)
}
await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,
data: user,
req,
returning: false,
})
user.collection = collectionConfig.slug
user._strategy = 'local-jwt'
fieldsToSignArgs.sid = newSessionID
if (sid) {
fieldsToSignArgs.sid = sid
}
const fieldsToSign = getFieldsToSign(fieldsToSignArgs)

View File

@@ -73,6 +73,9 @@ export const logoutOperation = async (incomingArgs: Arguments): Promise<boolean>
userWithSessions.sessions = sessionsAfterLogout
}
// Ensure updatedAt date is always updated
;(userWithSessions as any).updatedAt = new Date().toISOString()
await req.payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,

View File

@@ -10,7 +10,7 @@ import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { getFieldsToSign } from '../getFieldsToSign.js'
import { jwtSign } from '../jwt.js'
import { removeExpiredSessions } from '../removeExpiredSessions.js'
import { removeExpiredSessions } from '../sessions.js'
export type Result = {
exp: number
@@ -92,6 +92,9 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
existingSession.expiresAt = new Date(now.getTime() + tokenExpInMs)
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await req.payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,

View File

@@ -131,6 +131,9 @@ export const resetPasswordOperation = async <TSlug extends CollectionSlug>(
// Update new password
// /////////////////////////////////////
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
const doc = await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,

View File

@@ -46,6 +46,9 @@ export const verifyEmailOperation = async (args: Args): Promise<boolean> => {
throw new APIError('Verification token is invalid.', httpStatus.FORBIDDEN)
}
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await req.payload.db.updateOne({
id: user.id,
collection: collection.config.slug,

View File

@@ -1,10 +0,0 @@
import type { UserSession } from './types.js'
export const removeExpiredSessions = (sessions: UserSession[]) => {
const now = new Date()
return sessions.filter(({ expiresAt }) => {
const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt)
return expiry > now
})
}

View File

@@ -0,0 +1,70 @@
import { v4 as uuid } from 'uuid'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { TypedUser } from '../index.js'
import type { Payload, PayloadRequest } from '../types/index.js'
import type { UserSession } from './types.js'
/**
* Removes expired sessions from an array of sessions
*/
export const removeExpiredSessions = (sessions: UserSession[]) => {
const now = new Date()
return sessions.filter(({ expiresAt }) => {
const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt)
return expiry > now
})
}
/**
* Adds a session to the user and removes expired sessions
* @returns The session ID (sid) if sessions are used
*/
export const addSessionToUser = async ({
collectionConfig,
payload,
req,
user,
}: {
collectionConfig: SanitizedCollectionConfig
payload: Payload
req: PayloadRequest
user: TypedUser
}): Promise<{ sid?: string }> => {
let sid: string | undefined
if (collectionConfig.auth.useSessions) {
// Add session to user
sid = uuid()
const now = new Date()
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
const expiresAt = new Date(now.getTime() + tokenExpInMs)
const session = { id: sid, createdAt: now, expiresAt }
if (!user.sessions?.length) {
user.sessions = [session]
} else {
user.sessions = removeExpiredSessions(user.sessions)
user.sessions.push(session)
}
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,
data: user,
req,
returning: false,
})
user.collection = collectionConfig.slug
user._strategy = 'local-jwt'
}
return {
sid,
}
}

View File

@@ -142,6 +142,9 @@ export const incrementLoginAttempts = async ({
user.sessions = currentUser.sessions
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await payload.db.updateOne({
id: user.id,
collection: collection.slug,

View File

@@ -29,7 +29,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
export type ServerOnlyCollectionAdminProperties = keyof Pick<
SanitizedCollectionConfig['admin'],
'baseListFilter' | 'components' | 'hidden'
'baseFilter' | 'baseListFilter' | 'components' | 'hidden'
>
export type ServerOnlyUploadProperties = keyof Pick<
@@ -94,6 +94,7 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
'hidden',
'baseFilter',
'baseListFilter',
'components',
// 'preview' is handled separately

View File

@@ -270,7 +270,7 @@ export type EnableFoldersOptions = {
debug?: boolean
}
export type BaseListFilter = (args: {
export type BaseFilter = (args: {
limit: number
locale?: TypedLocale
page: number
@@ -278,7 +278,31 @@ export type BaseListFilter = (args: {
sort: string
}) => null | Promise<null | Where> | Where
/**
* @deprecated Use `BaseFilter` instead.
*/
export type BaseListFilter = BaseFilter
export type CollectionAdminOptions = {
/**
* Defines a default base filter which will be applied in the following parts of the admin panel:
* - List View
* - Relationship fields for internal links within the Lexical editor
*
* This is especially useful for plugins like multi-tenant. For example,
* a user may have access to multiple tenants, but should only see content
* related to the currently active or selected tenant in those places.
*/
baseFilter?: BaseFilter
/**
* @deprecated Use `baseFilter` instead. If both are defined,
* `baseFilter` will take precedence. This property remains only
* for backward compatibility and may be removed in a future version.
*
* Originally, `baseListFilter` was intended to filter only the List View
* in the admin panel. However, base filtering is often required in other areas
* such as internal link relationships in the Lexical editor.
*/
baseListFilter?: BaseListFilter
/**
* Custom admin components
@@ -688,7 +712,7 @@ export type AuthCollection = {
}
export type TypeWithID = {
deletedAt?: string
deletedAt?: null | string
docId?: any
id: number | string
}
@@ -696,7 +720,7 @@ export type TypeWithID = {
export type TypeWithTimestamps = {
[key: string]: unknown
createdAt: string
deletedAt?: string
deletedAt?: null | string
id: number | string
updatedAt: string
}

View File

@@ -32,7 +32,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -32,7 +32,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -81,7 +81,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -46,7 +46,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -62,7 +62,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -76,7 +76,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -77,7 +77,7 @@ export type Options<
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -54,7 +54,7 @@ export type Options<
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-restricted-exports */
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, PopulateType, SelectType } from '../../../types/index.js'
import type { CreateLocalReqOptions } from '../../../utilities/createLocalReq.js'
@@ -48,7 +47,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-restricted-exports */
import type { PaginatedDocs } from '../../../database/types.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type {
@@ -53,7 +52,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -41,7 +41,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -76,7 +76,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -258,6 +258,8 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
select: incomingSelect,
})
// Ensure updatedAt date is always updated
result.updatedAt = new Date().toISOString()
result = await req.payload.db.updateOne({
id: parentDocID,
collection: collectionConfig.slug,

View File

@@ -293,6 +293,8 @@ export const updateDocument = async <
// /////////////////////////////////////
if (!shouldSaveDraft) {
// Ensure updatedAt date is always updated
dataToUpdate.updatedAt = new Date().toISOString()
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,

View File

@@ -402,6 +402,7 @@ export type Params = { [key: string]: string | string[] | undefined }
export type ServerProps = {
readonly documentSubViewType?: DocumentSubViewTypes
readonly i18n: I18nClient
readonly id?: number | string
readonly locale?: Locale
readonly params?: Params
readonly payload: Payload

View File

@@ -162,7 +162,12 @@ export async function validateSearchParam({
if (versionFields) {
fieldAccess = policies[entityType]![entitySlug]!.fields
if (segments[0] === 'parent' || segments[0] === 'version' || segments[0] === 'snapshot') {
if (
segments[0] === 'parent' ||
segments[0] === 'version' ||
segments[0] === 'snapshot' ||
segments[0] === 'latest'
) {
segments.shift()
}
} else {

View File

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

View File

@@ -6,6 +6,7 @@ export {
parseCookies,
} from '../auth/cookies.js'
export { getLoginOptions } from '../auth/getLoginOptions.js'
export { addSessionToUser, removeExpiredSessions } from '../auth/sessions.js'
export { getFromImportMap } from '../bin/generateImportMap/utilities/getFromImportMap.js'
export { parsePayloadComponent } from '../bin/generateImportMap/utilities/parsePayloadComponent.js'
export { defaults as collectionDefaults } from '../collections/config/defaults.js'

View File

@@ -2,8 +2,7 @@ import ObjectIdImport from 'bson-objectid'
import type { TextField } from '../config/types.js'
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
export const baseIDField: TextField = {
name: 'id',
@@ -14,6 +13,7 @@ export const baseIDField: TextField = {
defaultValue: () => new ObjectId().toHexString(),
hooks: {
beforeChange: [({ value }) => value || new ObjectId().toHexString()],
// ID field values for arrays and blocks need to be unique when duplicating, as on postgres they are stored on the same table as primary keys.
beforeDuplicate: [() => new ObjectId().toHexString()],
},
label: 'ID',

View File

@@ -49,6 +49,9 @@ export function getFieldPaths({
}
}
/**
* @deprecated - will be removed in 4.0. Use `getFieldPaths` instead.
*/
export function getFieldPathsModified({
field,
index,

View File

@@ -111,13 +111,14 @@ export const promise = async ({
parentSchemaPath,
})
const fieldAffectsDataResult = fieldAffectsData(field)
const pathSegments = path ? path.split('.') : []
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
let removedFieldValue = false
if (
fieldAffectsData(field) &&
fieldAffectsDataResult &&
field.hidden &&
typeof siblingDoc[field.name!] !== 'undefined' &&
!showHiddenFields
@@ -139,16 +140,17 @@ export const promise = async ({
}
}
const shouldHoistLocalizedValue =
const shouldHoistLocalizedValue: boolean = Boolean(
flattenLocales &&
fieldAffectsData(field) &&
typeof siblingDoc[field.name!] === 'object' &&
siblingDoc[field.name!] !== null &&
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
locale !== 'all' &&
req.payload.config.localization
fieldAffectsDataResult &&
typeof siblingDoc[field.name!] === 'object' &&
siblingDoc[field.name!] !== null &&
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
locale !== 'all' &&
req.payload.config.localization,
)
if (shouldHoistLocalizedValue) {
if (fieldAffectsDataResult && shouldHoistLocalizedValue) {
// replace actual value with localized value before sanitizing
// { [locale]: fields } -> fields
const value = siblingDoc[field.name!][locale!]
@@ -187,7 +189,7 @@ export const promise = async ({
case 'group': {
// Fill groups with empty objects so fields with hooks within groups can populate
// themselves virtually as necessary
if (fieldAffectsData(field) && typeof siblingDoc[field.name] === 'undefined') {
if (fieldAffectsDataResult && typeof siblingDoc[field.name] === 'undefined') {
siblingDoc[field.name] = {}
}
@@ -234,7 +236,7 @@ export const promise = async ({
}
}
if (fieldAffectsData(field)) {
if (fieldAffectsDataResult) {
// Execute hooks
if (triggerHooks && field.hooks?.afterRead) {
for (const hook of field.hooks.afterRead) {
@@ -400,7 +402,7 @@ export const promise = async ({
}
}
if (Array.isArray(rows)) {
if (Array.isArray(rows) && rows.length > 0) {
rows.forEach((row, rowIndex) => {
traverseFields({
blockData,
@@ -468,6 +470,8 @@ export const promise = async ({
})
}
})
} else if (shouldHoistLocalizedValue && (!rows || rows.length === 0)) {
siblingDoc[field.name] = null
} else if (field.hidden !== true || showHiddenFields === true) {
siblingDoc[field.name] = []
}
@@ -477,7 +481,7 @@ export const promise = async ({
case 'blocks': {
const rows = siblingDoc[field.name]
if (Array.isArray(rows)) {
if (Array.isArray(rows) && rows.length > 0) {
rows.forEach((row, rowIndex) => {
const blockTypeToMatch = (row as JsonObject).blockType
@@ -573,6 +577,8 @@ export const promise = async ({
})
}
})
} else if (shouldHoistLocalizedValue && (!rows || rows.length === 0)) {
siblingDoc[field.name] = null
} else if (field.hidden !== true || showHiddenFields === true) {
siblingDoc[field.name] = []
}
@@ -617,7 +623,7 @@ export const promise = async ({
}
case 'group': {
if (fieldAffectsData(field)) {
if (fieldAffectsDataResult) {
let groupDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') {

View File

@@ -63,7 +63,8 @@ export const promise = async <T>({
let fieldData = siblingDoc?.[field.name!]
const fieldIsLocalized = localization && fieldShouldBeLocalized({ field, parentIsLocalized })
// Run field beforeDuplicate hooks
// Run field beforeDuplicate hooks.
// These hooks are responsible for resetting the `id` field values of array and block rows. See `baseIDField`.
if (Array.isArray(field.hooks?.beforeDuplicate)) {
if (fieldIsLocalized) {
const localeData: JsonObject = {}

View File

@@ -1,8 +1,7 @@
import Ajv from 'ajv'
import ObjectIdImport from 'bson-objectid'
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
import type { TFunction } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'

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