Compare commits

..

69 Commits

Author SHA1 Message Date
Elliot DeNolf
070c4f7e55 chore: regenerate templates 2025-04-11 13:36:27 -04:00
Elliot DeNolf
b74b14e119 chore(deps): bump sharp for security vulnerability in tar-fs 2025-04-11 13:28:58 -04:00
Paul
3a7cd717b2 fix(ui): issue with schedule publish disappearing on autosave collections (#12078)
Fixes an issue where an autosave being triggered would turn off the
ability to schedule a publish. This happened because we check against
`modified` on the form but with autosave modified is always true.

Now we make an exception for autosave enabled collections when checking
the modified state.
2025-04-11 10:43:40 -04:00
Slava Nossar
3287f7062f fix(ui): use route.api from config in OrderableTable (#12081)
### What?
`OrderableTable` doesn't respect a user-sepcified `routes.api` value and
instead uses the default `/api`

### Why?
See #12080

### How?
Gets `config` via `useConfig`, and uses `config.routes.api` in the
`fetch` for reordering.

Fixes #12080
2025-04-11 06:03:39 -03:00
Corey Larson
a9eca3a785 fix: correct typo in error message and remove console.log (#12082)
### What?

This PR corrects a typo in an error message and removes a console.log from the `orderBeforeChangeHook` hook.

### Why?

An error message contains a typo, and every time I reorder an orderable collection, `do not enter` gets logged.

<img width="153" alt="Screenshot 2025-04-11 at 1 11 29 AM" src="https://github.com/user-attachments/assets/13ae106b-0bb9-4421-9083-330d3b6f356d" />
2025-04-11 08:42:39 +00:00
alexrah
71e3c7839b fix(db-postgres): use correct export path for codegen in createSchemaGenerator (#12043)
following changes made by Commit a6f7ef8

> feat(db-*): export types from main export (#11914)
In 3.0, we made the decision to export all types from the main package
export (e.g. `payload/types` => `payload`). This improves type
discoverability by IDEs and simplifies importing types.

> This PR does the same for our db adapters, which still have a separate
`/types` subpath export. While those are kept for
backwards-compatibility, we can remove them in 4.0.


a6f7ef837a


the script responsible for generating file generated-schema.ts was not
updated to reflect this change in export paths

drizzle/src/utilities/createSchemaGenerator.ts

CURRENT 
```typescript
    const finalDeclaration = `
declare module '${this.packageName}/types' {
  export interface GeneratedDatabaseSchema {
    schema: DatabaseSchema
  }
}
```

AFTER THIS PULL REQUEST
```typescript
    const finalDeclaration = `
declare module '${this.packageName}' {
  export interface GeneratedDatabaseSchema {
    schema: DatabaseSchema
  }
}
```

this pull request fixes the generation of generated-schema.ts avoiding
errors while building for production with command
```bash
npm run build
```
![Screenshot 2025-04-08 at 17 00
11](https://github.com/user-attachments/assets/203de476-0f8f-4d65-90e6-58c50bd3e2a6)
2025-04-11 10:58:55 +03:00
Germán Jabloñski
a66f90ebb6 chore: separate Lexical tests into dedicated suite (#12047)
Lexical tests comprise almost half of the collections in the fields
suite, and are starting to become complex to manage.

They are sometimes related to other auxiliary collections, so
refactoring one test sometimes breaks another, seemingly unrelated one.

In addition, the fields suite is very large, taking a long time to
compile. This will make it faster.

Some ideas for future refactorings:
- 3 main collections: defaultFeatures, fully featured, and legacy.
Legacy is the current one that has multiple editors and could later be
migrated to the first two.
- Avoid collections with more than 1 editor.
- Create reseed buttons to restore the editor to certain states, to
avoid a proliferation of collections and documents.
- Reduce the complexity of the three auxiliary collections (text, array,
upload), which are rarely or never used and have many fields designed
for tests in the fields suite.
2025-04-10 20:47:26 -03:00
Elliot DeNolf
272914c818 chore(release): v3.34.0 [skip ci] 2025-04-10 15:38:35 -04:00
Sasha
466dcd7189 feat: support where querying by join fields (#12075)
### What?
This PR adds support for `where` querying by the join field (don't
confuse with `where` querying of related docs via `joins.where`)

Previously, this didn't work:
```
const categories = await payload.find({
  collection: 'categories',
  where: { 'relatedPosts.title': { equals: 'my-title' } },
})
```

### Why?
This is crucial for bi-directional relationships, can be used for access
control.

### How?
Implements `where` handling for join fields the same as we do for
relationships. In MongoDB it's not as efficient as it can be, the old PR
that improves it and can be updated later is here
https://github.com/payloadcms/payload/pull/8858

Fixes https://github.com/payloadcms/payload/discussions/9683
2025-04-10 15:30:40 -04:00
Germán Jabloñski
a72fa869f3 chore(plugin-seo): enable TypeScript strict (#11933) 2025-04-10 15:12:44 -04:00
Paul
3523c2c6a6 templates: update readme on blank template and blank template variations for Vercel (#12070)
Updated the readmes on our blank template so it's closer to what we have
on the website
template.

Updated the Vercel variation ones as well because those are used
directly for the Vercel marketplace.
2025-04-10 19:11:28 +01:00
Patrik
112e081d8f fix(ui): ensure file field is only serialized at top-level for upload-enabled collections (#12074)
This fixes an issue where fields with the name `file` was being
serialized as a top-level field in multipart form data even when the
collection was not upload-enabled. This caused the value of `file` (when
used as a regular field like a text, array, etc.) to be stripped from
the `_payload`.

- Updated `createFormData` to only delete `data.file` and serialize it
at the top level if `docConfig.upload` is defined.
- This prevents unintended loss of `file` field values for non-upload
collections.

The `file` field now remains safely nested in `_payload` unless it's
part of an upload-enabled collection.
2025-04-10 17:37:10 +00:00
Paul
eab9770315 feat: add support for time format config on scheduled publish (#12073)
This PR adds a new `SchedulePublish` config type on our schedulePublish
configuration in versions from being just boolean.

Two new options are supported:
- `timeFormat` which controls the formatting of the time slots, allowing
users to change from a 12-hour clock to a 24-hour clock (default to 12
hour)
- `timeIntervals` which controls the generated time slots (default 5)

Example configuration:

```
versions: {
  drafts: {
    schedulePublish: {
      timeFormat: 'HH:mm',
      timeIntervals: 5,
    },
  },
},
```
2025-04-10 18:22:21 +01:00
Jacob Fletcher
4d7c1d45fa fix(ui): form state race conditions (#12026)
Fixes form state race conditions. Modifying state while a request is in
flight or while the response is being processed could result in those
changes being overridden.

This was happening for a few reasons:

1. Our merge logic was incorrect. We were disregarding local changes to
state that may have occurred while form state requests are pending. This
was because we were iterating over local state, then while building up
new state, we were ignoring any fields that did not exist in the server
response, like this:
    
    ```ts
    for (const [path, newFieldState] of Object.entries(existingState)) {
    
      if (!incomingState[path]) {
        continue
      }
      
      // ...
    }
    ```

To fix this, we need to use local state as the source of truth. Then
when the server state arrives, we need to iterate over _that_. If a
field matches in local state, merge in any new properties. This will
ensure all changes to the underlying state are preserved, including any
potential addition or deletions.
    
However, this logic breaks down if the server might have created _new_
fields, like when populating array rows. This means they, too, would be
ignored. To get around this, there is a new `addedByServer` property
that flags new fields to ensure they are kept.
    
This new merge strategy also saves an additional loop over form state.
    
1. We were merging form state based on a mutable ref. This meant that
changes made within one action cause concurrent actions to have dirty
reads. The fix for this is to merge in an isolated manner by copying
state. This will remove any object references. It is generally not good
practice to mutate state without setting it, anyways, as this causes
mismatches between what is rendered and what is in memory.
    
1. We were merging server form state directly within an effect, then
replacing state entirely. This meant that if another action took place
at the exact moment in time _after_ merge but _before_ dispatch, the
results of that other action would be completely overridden. The fix for
this is to perform the merge within the reducer itself. This will ensure
that we are working with a trustworthy snapshot of state at the exact
moment in time that the action was invoked, and that React can properly
queue the event within its lifecycle.
2025-04-10 12:11:54 -04:00
Paul
37bfc63da2 chore(deps): bump image-size to 2.0.2 version (#12063)
Bumps our `image-size` dependency to 2.0.2 which includes the [DDOS
fix](https://github.com/payloadcms/payload/pull/12040) previously
released.

The [2.0](https://github.com/image-size/image-size/releases/tag/v2.0.0)
of this library comes with some benefits such as no dependencies and
improved performance.
2025-04-10 14:29:44 +01:00
Patrik
18ff9cbdb1 fix(ui): adds multi select inputs for text fields in where builder (#12054)
### What?

The `in` & `not_in` operators were not properly working for `text`
fields as this operator requires an array of values for it's input.

### How?

Conditionally renders a multi select input for `text` fields when
filtering by `in` & `not_in` operators.
2025-04-10 08:54:50 -04:00
Germán Jabloñski
ae9e5e19ad ci: add sort and hooks suites to the e2e tests matrix (#12023)
Trying to understand why bug #12002 arose, I found that both the `sort`
and `hooks` test suites are not running in CI.

I'm adding those 2 suites to the array, though later we should find a
way to automate this so it doesn't happen again. Manually rewriting all
test suites in the GitHub action is error-prone. It's very easy to
forget to add it when creating a new test suite
2025-04-10 09:51:24 -03:00
Sasha
7aa3c5ea6b fix: cannot define a join field when the target relationship is nested to a second or higher tab (#12041)
Fixes https://github.com/payloadcms/payload/issues/11720
2025-04-10 15:36:03 +03:00
Jessica Chowdhury
a0fb3353c6 fix: image previews getting stuck in list view when paginating (#12062)
### What?
In the List View, row data related to images and relationships gets
stuck when you go from one page to another.

### Why?
The `key` we are providing is not unique and not triggering the DOM to
update.

### How?
Uses the `row id` as a unique key prop to each table row to ensure
proper re-rendering of rows during pagination.

#### Testing
Adds e2e test to `upload` test suite. You can recreate the issue using
the `upload` test suite and new `list view preview` collection.
2025-04-10 13:18:10 +01:00
Philipp Schneider
101f7658f7 perf(richtext-lexical): debounce field onChange handler (#12046)
On devices without a top-notch CPU, typing in the rich text editor is
laggy even in the very basic community test suite's "Post" collection.
Lags can be up to multiple seconds. This lag can be reproduced by e.g.
throttling the CPU by 6x on a MacBook Pro with M1 Pro chip and 32GB of
RAM. Typing at regular speed already stutters, and the Chromium
performance monitor shows 100% peak CPU utilization. Under the same
circumstances, the Lexical rich text editor on
https://playground.lexical.dev/ does not exhibit the same laggy UI
reactions.

The issue was narrowed down to the editor state serialization that was
so far executed on every change in `Field.tsx` and utilizing more than 1
frame's worth of CPU time.

This PR attempts to address the issue by asking the browser to queue the
work in moments where it doesn't interfere with UI responsiveness, via
`requestIdleCallback`.

To verify this change, simulate a slow CPU by setting `CPU: 6x slowdown`
in the Chromium `Performance` Dev Tool panel, and then type into the
community test suite's example post's rich text field.

I did not collect exhaustive benchmarks, since numbers are system
specific and the impact of the code change is simple to verify.

Demos:

Before, whole words are not appearing while typing, but then appear all
at once, INP is 6s, and CPU at 100% basically the whole interaction
time:


https://github.com/user-attachments/assets/535653d5-c9e6-4189-a0e0-f71d39c43c31

After: Most letters appear without delay, individual letters can be
slightly delayed, but INP is much more reasonable 350ms, and CPU has
enough bandwidth to drop below 100% utilization:


https://github.com/user-attachments/assets/e627bf50-b441-41de-b3a3-7ba5443ff049

⬆️ This recording is from an earlier solution attempt with 500ms
debouncing. The current approach with `requestIdleCallback` increases
CPU usage back to a close 100%, but the INP is further reduced to 2xxms
on my machine, and the perceived UI laggyness is comparable to this
recording.

---

This PR only addresses the rich text editor, because that's where the
performance was a severe usability deal-breaker for real world usage.
Presumably other input fields where users trigger a lot of change events
in succession such as text, textarea, number, and JSON fields might also
benefit from similar debouncing.
2025-04-10 08:41:37 -03:00
Jarrod Flesch
9853f27667 fix(ui): orderable table rendering (#12066)
Adds components used in the renderTable component to the client exports.
2025-04-09 23:48:45 -04:00
Alessio Gravili
e0046bba59 chore(deps): bump next.js to 15.3.0 and related dependencies (#12067)
This unblocks https://github.com/payloadcms/payload/pull/11376 and
guarantees support for Next.js 15.3.0
2025-04-09 21:42:45 +00:00
Alessio Gravili
f1d9b44161 fix(richtext-lexical): diff component css was not included in css bundle (#12028)
Currently, the lexical version diff component is completely unstyled, as
the scss was never included in our css bundle. This PR ensures that the
diff component scss is included in our css bundle
2025-04-09 18:32:21 +00:00
Patrik
09916ad18e fix(ui): adds multi select inputs for number fields in where builder (#12053)
### What?

The `in` & `not_in` operators were not properly working for `number`
fields as this operator requires an array of values for it's input.

### How?

Conditionally renders a multi select input for `number` fields when
filtering by `in` & `not_in` operators.
2025-04-09 13:26:18 -04:00
Jessica Chowdhury
a90ae9d42b docs: formatting tweaks for local api docs (#12064)
More formatting cleanup for new Local API / server function docs.
2025-04-09 17:01:29 +01:00
Tylan Davis
d19412f62d docs: adjust formatting on Local API - Server Functions documentation (#12058)
### What?

Adjusts markdown formatting on Local API - Server Functions
documentation

### Why?

Some unnecessary characters and duplicate headline values causing issues
on website frontend.

### How?

Removes unnecessary characters and adds unique anchor tags for duplicate
headlines.
2025-04-09 09:27:51 -04:00
Jacob Fletcher
bd557a97d5 test: optimistic form state rows (#12055)
Adds tests for #11961.
2025-04-08 20:56:24 -06:00
Germán Jabloñski
97e2e77ff4 chore: run dev:generate-types (#11994) 2025-04-08 17:25:29 -03:00
Paul
acae547ddf chore(deps): bump image-size package for security update (#12040)
[v1.2.1](https://github.com/image-size/image-size/releases/tag/v1.2.1)
releases a security patch for the `image-size` package
2025-04-08 13:33:42 -04:00
Jessica Chowdhury
ec34e64261 fix(ui): resets value in where builder when operator changes (#11136)
### What?
The list filters in the collection view allows invalid queries. If you
enter a value and then change operator, the value will remain even if it
doesn't pass the new value field validation, and an error is thrown.

### Why?
The value isn't reset or revalidated on operator change. It is reset on
field change.

### How?
Resets the value field when the operator changes.

Fixes #10648
2025-04-08 14:52:11 +01:00
Jessica Chowdhury
f079eced8a fix: array minRow validation should not show when non-required with no rows (#12037)
### What?
UI only issue: An array row with `required: false` and `minRows: x` was
displaying an error banner - this should only happen if one or more rows
are present.

The validation is not affected, the document still saved as expected,
but the error should not be inaccurately displayed.

### Why?
The logic for displaying the `minRow` validation error was `rows.length
> minRows` and it needs to be `rows.length > 1 && rows.length > minRows`

### How?
Updates the UI logic.

Fixes #12010
2025-04-08 13:47:29 +00:00
Jessica Chowdhury
b809c98966 docs: adds server function and access control sections to local API docs (#11902)
### What?
Adds 2 new topics to our Local API docs:
- Using server functions with local API ops
- Respecting access control

Will also be updating the server function docs with `reusable server
functions` once https://github.com/payloadcms/payload/pull/11900 is
merged.
2025-04-08 10:44:40 +01:00
Sasha
b9ffbc6994 fix: querying by polymorphic join field relationTo with overrideAccess: false (#11999)
Previously, querying by polymorphic joins `relationTo` with
`overrideAccess: false` caused an error:
```
QueryError: The following paths cannot be queried: relationTo
```

As this field actually doesn't exist in the schema. Now, under condition
that the query comes from a polymorphic join we skip checking
`relationTo` field access.
2025-04-07 20:19:43 +00:00
Sasha
09782be0e0 fix(db-postgres): long array field table aliases cause error even when dbName is used (#11995)
Fixes https://github.com/payloadcms/payload/issues/11975

Previously, this configuration was causing errors in postgres due to
long names, even though `dbName` is used:
```
{
  slug: 'aliases',
  fields: [
    {
      name: 'thisIsALongFieldNameThatWillCauseAPostgresErrorEvenThoughWeSetAShorterDBName',
      dbName: 'shortname',
      type: 'array',
      fields: [
        {
          name: 'nested_field_1',
          type: 'array',
          dbName: 'short_nested_1',
          fields: [],
        },
        {
          name: 'nested_field_2',
          type: 'text',
        },
      ],
    },
  ],
},
```

This is because we were generating Drizzle relation name (for arrays)
always based on the field path and internally, drizzle uses this name
for aliasing. Now, if `dbName` is present, we use `_{dbName}` instead
for the relation name.
2025-04-07 20:12:43 +00:00
Paul
b270901fa6 chore: add logging templates script and fix engines for pnpm v10 (#12021)
- Fixes issues with using pnpm v10 in some templates by allowing `^10`
in engines as well
- Added logging to the template generation script so we can debug the
latest version being pulled by CI
2025-04-07 15:13:54 -04:00
Patrik
c7b14bd44d fix(ui): upload edits handling for bulk uploads (#12001)
### What?

This PR addresses a bug where image edits (crop, focal point, etc.) were
not persisting correctly in bulk uploads due to shared state logic with
single uploads.

### How?

- The `Upload` component now receives `uploadEdits`, `resetUploadEdits`,
and `updateUploadEdits` as props.
- `Upload_v4` was introduced to encapsulate the actual upload logic,
making it easier to reuse and test.
- The `AddingFilesView` and `EditForm` components are responsible for
injecting the correct `uploadEdits` state, depending on context.
- Avoided unnecessary `useFormsManager` usage in `Upload`.

Fixes #11868
2025-04-07 14:06:39 -04:00
Patrik
83319be752 docs: clarify file upload example with _payload & field explanation (#12025)
### What?

This PR updates the `Uploading Files` section in the `Uploads` docs to:

- Use `_payload` in the file upload example, which is required for
non-file fields to be parsed correctly by Payload.
- Add a clear comment explaining that the fields inside `_payload`
should match the schema of the upload-enabled collection.

### Why?

These changes aim to reduce confusion when uploading files via the REST
API.

Fixes #11681
2025-04-07 14:06:03 -04:00
Said Akhrarov
77210251f4 fix(ui): prefer adminThumbnail even if file is non-image (#11948)
### What?

This PR relaxes the mimeType checks in the thumbnail and file cell
components to accommodate an `adminThumbnail` even if the file is a
non-image. This is useful when, for example, using an `adminThumbnail`
function to retrieve or generate thumbnails for files that are
non-images such as videos.

### Why?
To prioritize an admin thumbnail if/when available on file cells and
upload field thumbnails in both edit and list views.

### How?

By relaxing the mimeType checks in the `Thumbnail` component and instead
lifting that responsibility on the caller of this component. Some of
these checks were not needed as the best-fit helper utility function
will automatically select the thumbnailURL if available or revert to the
original url if no best-fit is found.

Demo of admin thumbnail being loaded on non-image while still selecting
best-fit size for images:

![chrome_2025-04-01_18-56-25](https://github.com/user-attachments/assets/befd3647-92c5-45c6-90e2-87459bca8bea)
2025-04-07 13:43:25 -04:00
Jacob Fletcher
750210fabe test: temp skip blocks e2e (#11988) 2025-04-07 11:15:08 -04:00
Elliot DeNolf
6d831475a0 templates: bump for v3.33.0 (#12003)
Manual bump of templates. Possible issue from #11992
2025-04-07 11:46:46 +01:00
Said Akhrarov
e109491dbe docs: fix and normalize links (#11993)
<!--

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 fixes a few links around the docs. It also normalizes some links
to use lowercase link-to sections.

### Why?
To send users to the correct location in the docs.

### How?
Changes to a few files in `docs/`
2025-04-06 01:13:56 +01:00
Omar
dee9abd5c1 docs: fix a typo (#12012)
Fix a typo

<!--

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-04-06 00:13:03 +00:00
zy1p
5c54d9a567 docs: fix markdown link (#12000)
### What?
<img width="749" alt="image"
src="https://github.com/user-attachments/assets/a9b6243d-2c50-48bc-a1a1-6a163949ec4a"
/>

Link not showing properly.
Check
https://payloadcms.com/docs/getting-started/installation#2-copy-payload-files-into-your-nextjs-app-folder

<img width="714" alt="image"
src="https://github.com/user-attachments/assets/d1c77d5f-ed3a-4b92-94b6-86694ae7668e"
/>

SQLite Adapter could be added to this section
Check
https://payloadcms.com/docs/getting-started/installation#1-install-the-relevant-packages

### Why?
Wrong syntax

### How?
* fix markdown link
* add section for install sqlite adapter
2025-04-04 20:20:56 +00:00
Elliot DeNolf
36e7c59b4e chore(release): v3.33.0 [skip ci] 2025-04-04 14:52:55 -04:00
Dan Ribbens
9adbbde9a8 fix: postgres null value breaks orderable hook (#11997)
When postgres is used and orderable is enabled, payload cannot update
the docs to set the order correctly. This is because the sort on
postgres pushes `null` values to the top causing unique constraints to
error when two documents are updated to the same _order value.
2025-04-04 14:31:34 -04:00
Sasha
8ad22eb1c0 fix: allow custom password field when using disableLocalStrategy: true (#11893)
Fixes https://github.com/payloadcms/payload/issues/11888

Previously, if you had `disableLocalStategy: true` and a custom
`password` field, Payload would still control it in `update.ts` by
deleting. Now, we don't do that in this case, unless we have
`disableLocalStetegy.enableFields: true`.
2025-04-04 20:52:10 +03:00
Paul
b76844dac9 templates: set packageManager pnpm version for vercel templates (#11992)
There have been issues with deploying our templates to Vercel when we
rely on `engines.pnpm` configuration.

Vercel's deployments work best when we specify a `packageManager` in
`package.json` since we ship our templates without lockfiles that would
help Vercel determine the right package manager to use.

This PR adjusts the script so that it adds a `packageManager` with the
latest version of `pnpm` to our Vercel templates and removes the
`engines.pnpm` only for those variants.
2025-04-04 18:30:04 +01:00
Alessio Gravili
f7ed8e90e1 docs: fix invalid markdown (#11996) 2025-04-04 12:41:54 -04:00
Tony Tkachenko
e6aad5adfc docs: add missing comma (#11976)
Add missing comma
2025-04-04 00:04:32 +00:00
Sasha
4ebd3ce668 fix(db-postgres): deleteOne fails when the where query does not resolve to any document (#11632)
Previously, if you called `payload.db.deleteOne` with a `where` query
that does not resolve to anything, an error would be occurred.
2025-04-04 00:46:31 +03:00
James
fae113b799 chore: fix flake 2025-04-03 17:06:35 -04:00
Jacob Fletcher
e87521a376 perf(ui): significantly optimize form state component rendering, up to 96% smaller and 75% faster (#11946)
Significantly optimizes the component rendering strategy within the form
state endpoint by precisely rendering only the fields that require it.
This cuts down on server processing and network response sizes when
invoking form state requests **that manipulate array and block rows
which contain server components**, such as rich text fields, custom row
labels, etc. (results listed below).

Here's a breakdown of the issue:

Previously, when manipulating array and block fields, _all_ rows would
render any server components that might exist within them, including
rich text fields. This means that subsequent changes to these fields
would potentially _re-render_ those same components even if they don't
require it.

For example, if you have an array field with a rich text field within
it, adding the first row would cause the rich text field to render,
which is expected. However, when you add a second row, the rich text
field within the first row would render again unnecessarily along with
the new row.

This is especially noticeable for fields with many rows, where every
single row processes its server components and returns RSC data. And
this does not only affect nested rich text fields, but any custom
component defined on the field level, as these are handled in the same
way.

The reason this was necessary in the first place was to ensure that the
server components receive the proper data when they are rendered, such
as the row index and the row's data. Changing one of these rows could
cause the server component to receive the wrong data if it was not
freshly rendered.

While this is still a requirement that rows receive up-to-date props, it
is no longer necessary to render everything.

Here's a breakdown of the actual fix:

This change ensures that only the fields that are actually being
manipulated will be rendered, rather than all rows. The existing rows
will remain in memory on the client, while the newly rendered components
will return from the server. For example, if you add a new row to an
array field, only the new row will render its server components.

To do this, we send the path of the field that is being manipulated to
the server. The server can then use this path to determine for itself
which fields have already been rendered and which ones need required
rendering.

## Results

The following results were gathered by booting up the `form-state` test
suite and seeding 100 array rows, each containing a rich text field. To
invoke a form state request, we navigate to a document within the
"posts" collection, then add a new array row to the list. The result is
then saved to the file system for comparison.

| Test Suite | Collection | Number of Rows | Before | After | Percentage
Change |
|------|------|---------|--------|--------|--------|
| `form-state` | `posts` | 101 | 1.9MB / 266ms | 80KB / 70ms | ~96%
smaller / ~75% faster |

---------

Co-authored-by: James <james@trbl.design>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
2025-04-03 12:27:14 -04:00
Jacob Fletcher
8880d705e3 fix(ui): optimistic rows disappear while form state requests are pending (#11961)
When manipulating array and blocks rows on slow networks, rows can
sometimes disappear and then reappear as requests in the queue arrive.

Consider this scenario:

1. You add a row to form state: this pushes the row in local state
optimistically then triggers a long-running form state request
containing a single row
2. You add another row to form state: this pushes a second row into
local state optimistically then triggers another long-running form state
request containing two rows
3. The first form state request returns with a single row in the
response and replaces local state (which contained two rows)
4. AT THIS MOMENT IN TIME, THE SECOND ROW DISAPPEARS
5. The second form state request returns with two rows in the response
and replaces local state
6. THE UI IS NO LONGER STALE AND BOTH ROWS APPEAR AS EXPECTED

The same issue applies when deleting, moving, and duplicating rows.
Local state becomes out of sync with the form state response and is
ultimately overridden.

The issue is that when we merge the result from form state, we do not
traverse the rows themselves, and instead take the rows in their
entirety. This means that we lose local row state. Instead, we need to
compare the results with what is saved to local state and intelligently
merge them.
2025-04-03 12:23:14 -04:00
reiv
018bdad247 feat(graphql): improve non-nullability in query result types (#11952)
### What?
Makes several fields and list item types in query results (e.g. `docs`)
non-nullable.

### Why?
When dealing with code generated from a Payload GraphQL schema, it is
often necessary to use type guards and optional chaining.

For example:

```graphql
type Posts {
  docs: [Post]
  ...
}
```

This implies that the `docs` field itself is nullable and that the array
can contain nulls. In reality, neither of these is true. But because of
the types generated by tools like `graphql-code-generator`, the way to
access `posts` ends up something like this:

```ts
const posts = (query.data.docs ?? []).filter(doc => doc != null);
```

Instead, we would like the schema to be:

```graphql
type Posts {
  docs: [Post!]!
  ...
}
```


### How?
The proposed change involves adding `GraphQLNonNull` where appropriate.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-04-03 15:17:23 +00:00
Said Akhrarov
816fb28f55 feat(ui): use drag overlay in orderable table (#11959)
<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR introduces a new `DragOverlay` to the existing `OrderableTable`
component along with a few new utility components. This enables a more
fluid and seamless drag-and-drop experience for end-users who have
enabled `orderable: true` on their collections.

### Why?
Previously, the rows in the `OrderableTable` component were confined
within the table element that renders them. This is troublesome for a
few reasons:
- It clips rows when dragging even slightly outside of the bounds of the
table.
- It creates unnecessary scrollbars within the containing element as the
container is not geared for comprehensive drag-and-drop interactions.

### How?
Introducing a `DragOverlay` component gives the draggable rows an area
to render freely without clipping. This PR also introduces a new
`OrderableRow` (for rendering orderable rows in the table as well as in
a drag preview), and an `OrderableRowDragPreview` component to render a
drag-preview of the active row 1:1 as you would see in the table without
violating HTML rules.

This PR also adds an `onDragStart` event handler to the
`DraggableDroppable` component to allow for listening for the start of a
drag event, necessary for interactions with a `DragOverlay` to
communicate which row initiated the event.

Before:


[orderable-before.webm](https://github.com/user-attachments/assets/ccf32bb0-91db-44f3-8c2a-4f81bb762529)


After:


[orderable-after.webm](https://github.com/user-attachments/assets/d320e7e6-fab8-4ea4-9cb1-38b581cbc50e)


After (With overflow on page):


[orderable-overflow-y.webm](https://github.com/user-attachments/assets/418b9018-901d-4217-980c-8d04d58d19c8)
2025-04-03 10:17:19 -03:00
Sasha
857e984fbb fix(db-mongodb): querying relationships with where clause as an object with several conditions (#11953)
Fixes https://github.com/payloadcms/payload/issues/11927

When trying to use the following notation:
```ts
const { docs } = await payload.find({
  collection: 'movies',
  depth: 0,
  where: {
    'director.name': { equals: 'Director1' },
    'director.localized': { equals: 'Director1_Localized' },
  },
})
```
Currently, it respects only the latest condition and the first is
ignored.

However, this works fine:
```ts
const { docs } = await payload.find({
  collection: 'movies',
  depth: 0,
  where: {
    and: [
      {
        'director.name': { equals: 'Director1' },
      },
      {
        'director.localized': { equals: 'Director1_Localized' },
      },
    ],
  },
})
```

But this should be an equivalent to
```
 where: {
    'director.name': { equals: 'Director1' },
    'director.localized': { equals: 'Director1_Localized' },
  },
```
2025-04-03 09:07:10 -04:00
Germán Jabloñski
d47b753898 chore(plugin-cloud-storage): enable TypeScript strict (#11850) 2025-04-03 10:06:25 -03:00
Germán Jabloñski
308cb64b9c chore(richtext-lexical): add DebugJsxConverterFeature (#10856)
Display the editor content below using the JSX converter
Added for debugging reasons, similar to TreeViewFeature

usage:

```ts
    {
      name: 'content',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [...defaultFeatures, DebugJsxConverterFeature()],
      }),
    },
```
2025-04-03 09:06:07 -04:00
Germán Jabloñski
6c735effff chore(plugin-redirects): enable TypeScript strict (#11931) 2025-04-03 09:04:21 -04:00
Germán Jabloñski
fd42ad5f52 chore(plugin-nested-docs): enable TypeScript strict (#11930) 2025-04-03 09:04:04 -04:00
Germán Jabloñski
a58ff57e4f chore(plugin-form-builder): enable TypeScript strict (#11929) 2025-04-03 09:01:13 -04:00
Alessio Gravili
06d937e903 docs: fix variable names for lexical markdown conversion (#11963) 2025-04-03 09:21:27 +03:00
Sasha
8e93ad8f5f fix(storage-uploadthing): pass clientUploads.routerInputConfig to the handler (#11962)
PR https://github.com/payloadcms/payload/pull/11954 added this property
but didn't actually pass it through to the handler.
2025-04-02 23:51:30 +00:00
Sasha
f310c90211 fix(db-postgres): down migration fails because migrationTableExists doesn't check in the current transaction (#11910)
Fixes https://github.com/payloadcms/payload/issues/11882

Previously, down migration that dropped the `payload_migrations` table
was failing because `migrationTableExists` doesn't check the current
transaction, only in which you can get a `false` value result.
2025-04-03 02:33:34 +03:00
Sasha
dc793d1d14 fix: ValidationError error message when label is a function (#11904)
Fixes https://github.com/payloadcms/payload/issues/11901

Previously, when `ValidationError` `errors.path` was referring to a
field with `label` defined as a function, the error message was
generated with `[object Object]`. Now, we call that function instead.
Since the `i18n` argument is required for `StaticLabel`, this PR
introduces so you can pass a partial `req` to `ValidationError` from
which we thread `req.i18n` to the label args.
2025-04-03 00:38:54 +03:00
Sasha
f9c73ad5f2 feat(storage-uploadthing): configurable upload router input config (#11954)
Fixes https://github.com/payloadcms/payload/issues/11949 by setting the
default limit to `512MB`.
Additionally, makes this configurable via
`clientUploads.routerInputConfig`. Details are here
https://docs.uploadthing.com/file-routes#route-config
2025-04-03 00:14:08 +03:00
Sasha
760cfadaad fix: do not append doc input for scheduled publish job if it's enabled only for globals (#11892)
Fixes https://github.com/payloadcms/payload/issues/11891


Previously, if you had scheduled publish enabled only for globals, not
collections - you'd get an error on `payload generate:types`:
<img width="886" alt="image"
src="https://github.com/user-attachments/assets/78125ce8-bd89-4269-bc56-966d8e0c3968"
/>

This was caused by appending the `doc` field to the scheduled publish
job input schema with empty `collections` array. Now we skip this field
if we don't have any collections.
2025-04-03 00:12:35 +03:00
Alessio Gravili
d29bdfc10f feat(next): improved lexical richText diffing in version view (#11760)
This replaces our JSON-based richtext diffing with HTML-based richtext
diffing for lexical. It uses [this HTML diff
library](https://github.com/Arman19941113/html-diff) that I then
modified to handle diffing more complex elements like links, uploads and
relationships.

This makes it way easier to spot changes, replacing the lengthy Lexical
JSON with a clean visual diff that shows exactly what's different.

## Before

![CleanShot 2025-03-18 at 13 54
51@2x](https://github.com/user-attachments/assets/811a7c14-d592-4fdc-a1f4-07eeb78255fe)


## After


![CleanShot 2025-03-31 at 18 14
10@2x](https://github.com/user-attachments/assets/efb64da0-4ff8-4965-a458-558a18375c46)
![CleanShot 2025-03-31 at 18 14
26@2x](https://github.com/user-attachments/assets/133652ce-503b-4b86-9c4c-e5c7706d8ea6)
2025-04-02 20:10:20 +00:00
Alessio Gravili
f34eb228c4 feat(drizzle): export buildQuery and parseParams (#11935)
This exports `buildQuery` and `parseParams` from @payloadcms/drizzle
2025-04-02 18:17:39 +00:00
422 changed files with 14766 additions and 5261 deletions

View File

@@ -294,14 +294,10 @@ jobs:
- fields__collections__Email
- fields__collections__Indexed
- fields__collections__JSON
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Lexical__e2e__blocks#config.blockreferences.ts
- fields__collections__Number
- fields__collections__Point
- fields__collections__Radio
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Row
- fields__collections__Select
- fields__collections__Tabs
@@ -309,6 +305,11 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- hooks
- lexical__collections__Lexical__e2e__main
- lexical__collections__Lexical__e2e__blocks
- lexical__collections__Lexical__e2e__blocks#config.blockreferences.ts
- lexical__collections__RichText
- query-presets
- form-state
- live-preview
@@ -320,6 +321,7 @@ jobs:
- plugin-import-export
- plugin-nested-docs
- plugin-seo
- sort
- versions
- uploads
env:

View File

@@ -21,10 +21,9 @@ When a user starts editing a document, Payload locks it for that user. If anothe
The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing.
<Banner type="info">
{' '}
**Note:** If your application does not require document locking, you can
disable this feature for any collection or global by setting the
`lockDocuments` property to `false`.{' '}
`lockDocuments` property to `false`.
</Banner>
### Config Options

View File

@@ -11,7 +11,7 @@ keywords: authentication, config, configuration, overview, documentation, Conten
title="Simplified Authentication for Headless CMS: Unlocking Reusability in One Line"
/>
Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), all well as your own external applications, completely eliminating the need for paid, third-party platforms and services.
Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), as well as your own external applications, completely eliminating the need for paid, third-party platforms and services.
Here are some common use cases of Authentication in your own applications:

View File

@@ -60,31 +60,31 @@ export const Posts: CollectionConfig = {
The following options are available:
| Option | Description |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
| Option | Description |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
_\* An asterisk denotes that a property is required._

View File

@@ -239,7 +239,7 @@ export default buildConfig({
// ...
// highlight-start
cors: {
origins: ['http://localhost:3000']
origins: ['http://localhost:3000'],
headers: ['x-custom-header']
}
// highlight-end

View File

@@ -6,7 +6,7 @@ desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Edit View is where users interact with individual [Collection](../collections/overview) and [Global](../globals/overview) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree.
The Edit View is where users interact with individual [Collection](../configuration/collections) and [Global](../configuration/globals) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree.
The Edit View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
@@ -103,12 +103,12 @@ The following options are available:
| Path | Description |
| ----------------- | -------------------------------------------------------------------------------------- |
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
| `Description` | A description of the Collection. [More details](#Description). |
| `Upload` | A file upload component. [More details](#Upload). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Collection. [More details](#description). |
| `Upload` | A file upload component. [More details](#upload). |
#### Globals
@@ -135,11 +135,11 @@ The following options are available:
| Path | Description |
| ----------------- | -------------------------------------------------------------------------------------- |
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
| `Description` | A description of the Global. [More details](#Description). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Global. [More details](#description). |
### SaveButton

View File

@@ -271,21 +271,6 @@ const result = await payload.find({
and blocks.
</Banner>
<Banner type="warning">
Currently, querying by the Join Field itself is not supported, meaning:
```ts
payload.find({
collection: 'categories',
where: {
'relatedPosts.title': { // relatedPosts is a join field
equals: "post"
}
}
})
```
does not work yet.
</Banner>
### Rest API
The REST API supports the same query options as the Local API. You can use the `joins` query parameter to customize the

View File

@@ -67,6 +67,11 @@ To install a Database Adapter, you can run **one** of the following commands:
pnpm i @payloadcms/db-postgres
```
- To install the [SQLite Adapter](../database/sqlite), run:
```bash
pnpm i @payloadcms/db-sqlite
```
<Banner type="success">
**Note:** New [Database Adapters](/docs/database/overview) are becoming
available every day. Check the docs for the most up-to-date list of what's
@@ -75,7 +80,7 @@ To install a Database Adapter, you can run **one** of the following commands:
#### 2. Copy Payload files into your Next.js app folder
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](<https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)>) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
```plaintext
app/

View File

@@ -0,0 +1,47 @@
---
title: Respecting Access Control with Local API Operations
label: Access Control
order: 40
desc: Learn how to implement and enforce access control in Payload's Local API operations, ensuring that the right permissions are respected during data manipulation.
keywords: server functions, local API, Payload, CMS, access control, permissions, user context, server-side logic, custom workflows, data management, headless CMS, TypeScript, Node.js, backend
---
In Payload, local API operations **override access control by default**. This means that operations will run without checking if the current user has permission to perform the action. This is useful in certain scenarios where access control is not necessary, but it is important to be aware of when to enforce it for security reasons.
### Default Behavior: Access Control Skipped
By default, **local API operations skip access control**. This allows operations to execute without the system checking if the current user has appropriate permissions. This might be helpful in admin or server-side scripts where the user context is not required to perform the operation.
#### For example:
```ts
// Access control is this operation would be skipped by default
const test = await payload.create({
collection: 'users',
data: {
email: 'test@test.com',
password: 'test',
},
})
```
### Respecting Access Control
If you want to respect access control and ensure that the operation is performed only if the user has appropriate permissions, you need to explicitly pass the `user` object and set the `overrideAccess` option to `false`.
- `overrideAccess: false`: This ensures that access control is **not skipped** and the operation respects the current user's permissions.
- `user`: Pass the authenticated user context to the operation. This ensures the system checks whether the user has the right permissions to perform the action.
```ts
const authedCreate = await payload.create({
collection: 'users',
overrideAccess: false, // This ensures access control will be applied
user, // Pass the authenticated user to check permissions
data: {
email: 'test@test.com',
password: 'test',
},
})
```
This example will only allow the document to be created if the `user` we passed has the appropriate access control permissions.

View File

@@ -0,0 +1,360 @@
---
title: Using Local API Operations with Server Functions
label: Server Functions
order: 30
desc: Learn to use Local API operations with Server Functions in Payload to manage server-side logic, data interactions, and custom workflows directly within your CMS.
keywords: server functions, local API, Payload, CMS, server-side logic, custom workflows, data management, headless CMS, TypeScript, Node.js, backend
---
In Next.js, **server functions** (previously called **server actions**) are special functions that run exclusively on the server, enabling secure backend logic execution while being callable from the frontend. These functions bridge the gap between client and server, allowing frontend components to perform backend operations without exposing sensitive logic.
### Why Use Server Functions?
- **Executing Backend Logic from the Frontend**: The Local API is designed for server environments and cannot be directly accessed from client-side code. Server functions enable frontend components to trigger backend operations securely.
- **Security Benefits**: Instead of exposing a full REST or GraphQL API, server functions restrict access to only the necessary operations, reducing potential security risks.
- **Performance Optimizations**: Next.js handles server functions efficiently, offering benefits like caching, optimized database queries, and reduced network overhead compared to traditional API calls.
- **Simplified Development Workflow**: Rather than setting up full API routes with authentication and authorization checks, server functions allow for lightweight, direct execution of necessary operations.
### When to Use Server Functions
Use server functions whenever you need to call Local API operations from the frontend. Since the Local API is only accessible from the backend, server functions act as a secure bridge, eliminating the need to expose additional API endpoints.
## Examples
All Local API operations can be used within server functions, allowing you to interact with Payload's backend securely.
For a full list of available operations, see the [Local API](https://payloadcms.com/docs/local-api/overview) overview.
In the following examples, we'll cover some common use cases, including:
- Creating a document
- Updating a document
- Handling file uploads when creating or updating a document
- Authenticating a user
### Creating a Document
First, let's create our server function. Here are some key points for this process:
- Begin by adding `'use server'` at the top of the file.
- You can still use utilities such as `getPayload()`.
- Once the function structure is in place, call the Local API operation `payload.create()` and pass in the relevant data.
- It's good practice to wrap this in a `try...catch` block for error handling.
- Finally, make sure to return the created document (don't just run the operation).
```ts
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function createPost(data) {
const payload = await getPayload({ config })
try {
const post = await payload.create({
collection: 'posts',
data,
})
return post
} catch (error) {
throw new Error(`Error creating post: ${error.message}`)
}
}
```
Now, let's look at how to call the `createPost` function we just created from the frontend in a React component when a user clicks a button:
```ts
'use client';
import React, { useState } from 'react';
import { createPost } from '../server/actions'; // import the server function
export const PostForm: React.FC = () => {
const [result, setResult] = useState<string>('');
return (
<>
<p>{result}</p>
<button
type="button"
onClick={async () => {
// Call the server function
const newPost = await createPost({ title: 'Sample Post' });
setResult('Post created: ' + newPost.title);
}}
>
Create Post
</button>
</>
);
};
```
### Updating a Document
The key points from the previous example also apply here.
To update a document instead of creating one, you would use `payload.update()` with the relevant data and **passing the document ID.**
Here's how the server function would look:
```ts
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function updatePost(id, data) {
const payload = await getPayload({ config })
try {
const post = await payload.update({
collection: 'posts',
id, // the document id is required
data,
})
return post
} catch (error) {
throw new Error(`Error updating post: ${error.message}`)
}
}
```
Here is how you would call the `updatePost` function from a frontend React component:
```ts
'use client';
import React, { useState } from 'react';
import { updatePost } from '../server/actions'; // import the server function
export const UpdatePostForm: React.FC = () => {
const [result, setResult] = useState<string>('');
return (
<>
<p>{result}</p>
<button
type="button"
onClick={async () => {
// Call the server function to update the post
const updatedPost = await updatePost('your-post-id-123', { title: 'Updated Post' });
setResult('Post updated: ' + updatedPost.title);
}}
>
Update Post
</button>
</>
);
};
```
### Authenticating a User
In this example, we will check if a user is authenticated using Payload's authentication system. Here's how it works:
- First, we use the headers function from `next/headers` to retrieve the request headers.
- Next, we pass these headers to `payload.auth()` to fetch the user's authentication details.
- If the user is authenticated, their information is returned. If not, handle the unauthenticated case accordingly.
Here's the server function to authenticate a user:
```ts
'use server'
import { headers as getHeaders } from 'next/headers'
import config from '@payload-config'
import { getPayload } from 'payload'
export const authenticateUser = async () => {
const payload = await getPayload({ config })
const headers = await getHeaders()
const { user } = await payload.auth({ headers })
if (user) {
return { hello: user.email }
}
return { hello: 'Not authenticated' }
}
```
Here's a basic example of how to call the authentication server function from the frontend to test it:
```ts
'use client';
import React, { useState } from 'react';
import { authenticateUser } from '../server/actions'; // Import the server function
export const AuthComponent: React.FC = () => {
const [userInfo, setUserInfo] = useState<string>('');
return (
<React.Fragment>
<p>{userInfo}</p>
<button
onClick={async () => {
// Call the server function to authenticate the user
const result = await authenticateUser();
setUserInfo(result.hello);
}}
type="button"
>
Check Authentication
</button>
</React.Fragment>
);
};
```
### Creating a Document with File Upload
This example demonstrates how to write a server function that creates a document with a file upload. Here are the key steps:
- Pass two arguments: **data** for the document content and **upload** for the file
- Merge the upload file into the document data as the media field
- Use `payload.create()` to create a new post document with both the document data and file
```ts
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function createPostWithUpload(data, upload) {
const payload = await getPayload({ config })
try {
// Prepare the data with the file
const postData = {
...data,
media: upload,
}
const post = await payload.create({
collection: 'posts',
data: postData,
})
return post
} catch (error) {
throw new Error(`Error creating post: ${error.message}`)
}
}
```
Here is how you would use the server function we just created in a frontend component to allow users to submit a post along with a file upload:
- The user enters the post title and selects a file to upload.
- When the form is submitted, the `handleSubmit` function checks if a file has been chosen.
- If a file is selected, it passes both the title and the file to the `createPostWithFile` server function.
- And you are done!
```ts
'use client';
import React, { useState } from 'react';
import { createPostWithUpload } from '../server/actions';
export const PostForm: React.FC = () => {
const [title, setTitle] = useState<string>('');
const [file, setFile] = useState<File | null>(null);
const [result, setResult] = useState<string>('');
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFile(e.target.files[0]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
setResult('Please upload a file.');
return;
}
try {
// Call the server function to create the post with the file
const newPost = await createPostWithUpload({ title }, file);
setResult('Post created with file: ' + newPost.title);
} catch (error) {
setResult('Error: ' + error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post Title"
/>
<input type="file" onChange={handleFileChange} />
<button type="submit">Create Post</button>
<p>{result}</p>
</form>
);
};
```
## Reusable Payload Server Functions
Coming soon…
## Error Handling in Server Functions
When using server functions, proper error handling is essential to prevent unhandled exceptions and provide meaningful feedback to the frontend.
### Best Practices#error-handling-best-practices
- Wrap Local API calls in **try/catch blocks** to catch potential errors.
- **Log errors** on the server for debugging purposes.
- Return structured **error responses** instead of exposing raw errors to the frontend.
Example of good error handling:
```ts
export async function createPost(data) {
try {
const payload = await getPayload({ config })
return await payload.create({ collection: 'posts', data })
} catch (error) {
console.error('Error creating post:', error)
return { error: 'Failed to create post' }
}
}
```
## Security Considerations
Using server functions helps prevent direct exposure of Local API operations to the frontend, but additional security best practices should be followed:
### Best Practices#security-best-practices
- **Restrict access**: Ensure that sensitive actions (like user management) are only callable by authorized users.
- **Avoid passing sensitive data**: Do not return sensitive information such as user data, passwords, etc.
- **Use authentication & authorization**: Check user roles before performing actions.
Example of restricting access based on user role:
```ts
export async function deletePost(postId, user) {
if (!user || user.role !== 'admin') {
throw new Error('Unauthorized')
}
const payload = await getPayload({ config })
return await payload.delete({ collection: 'posts', id: postId })
}
```

View File

@@ -55,18 +55,9 @@ Because _**you**_ are in complete control of who can do what with your data, you
wield that power responsibly before deploying to Production.
<Banner type="error">
**
By default, all Access Control functions require that a user is successfully logged in to
Payload to create, read, update, or delete data.
**
But, if you allow public user registration, for example, you will want to make sure that your
access control functions are more strict - permitting
**By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.**
**
only appropriate users
**
to perform appropriate actions.
But, if you allow public user registration, for example, you will want to make sure that your access control functions are more strict - permitting **only appropriate users** to perform appropriate actions.
</Banner>

View File

@@ -58,8 +58,8 @@ Query Presets are subject to the same [Access Control](../access-control/overvie
Access Control for Query Presets can be customized in two ways:
1. [Collection Access Control](#static-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
2. [Document Access Control](#dynamic-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
1. [Collection Access Control](#collection-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
### Collection Access Control

View File

@@ -21,7 +21,7 @@ import {
// Your richtext data here
const data: SerializedEditorState = {}
const html = convertLexicalToMarkdown({
const markdown = convertLexicalToMarkdown({
data,
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
@@ -101,7 +101,7 @@ import {
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
const html = convertMarkdownToLexical({
const lexicalJSON = convertMarkdownToLexical({
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),

View File

@@ -223,7 +223,7 @@ This allows you to add i18n translations scoped to your feature. This specific e
### Markdown Transformers#server-feature-markdown-transformers
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converters#markdown-lexical).
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converting-markdown).
```ts
import { createServerFeature } from '@payloadcms/richtext-lexical'

View File

@@ -334,12 +334,28 @@ To upload a file, use your collection's [`create`](/docs/rest-api/overview#colle
Send your request as a `multipart/form-data` request, using [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) if possible.
<Banner type="info">
**Note:** To include any additional fields (like `title`, `alt`, etc.), append
a `_payload` field containing a JSON-stringified object of the required
values. These values must match the schema of your upload-enabled collection.
</Banner>
```ts
const fileInput = document.querySelector('#your-file-input')
const formData = new FormData()
formData.append('file', fileInput.files[0])
// Replace with the fields defined in your upload-enabled collection.
// The example below includes an optional field like 'title'.
formData.append(
'_payload',
JSON.stringify({
title: 'Example Title',
description: 'An optional description for the file',
}),
)
fetch('api/:upload-slug', {
method: 'POST',
body: formData,

View File

@@ -31,7 +31,7 @@
"payload": "3.11.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"sharp": "0.32.6"
"sharp": "0.34.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",

View File

@@ -46,7 +46,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "7.45.4",
"sharp": "0.32.6",
"sharp": "0.34.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
},

View File

@@ -30,7 +30,7 @@
"qs-esm": "7.0.2",
"react": "19.0.0",
"react-dom": "19.0.0",
"sharp": "0.32.6"
"sharp": "0.34.0"
},
"devDependencies": {
"@payloadcms/graphql": "latest",

View File

@@ -31,7 +31,7 @@
"payload": "latest",
"react": "19.0.0",
"react-dom": "19.0.0",
"sharp": "0.32.6"
"sharp": "0.34.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",

View File

@@ -19,7 +19,7 @@
"payload-app": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sharp": "0.32.6"
"sharp": "0.34.0"
},
"devDependencies": {
"@remix-run/dev": "^2.15.2",

View File

@@ -25,7 +25,7 @@
"payload": "beta",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sharp": "0.32.6",
"sharp": "0.34.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
},

View File

@@ -24,7 +24,7 @@
"payload": "latest",
"react": "19.0.0",
"react-dom": "19.0.0",
"sharp": "0.32.6"
"sharp": "0.34.0"
},
"devDependencies": {
"@payloadcms/graphql": "latest",

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.32.0",
"version": "3.34.0",
"private": true,
"type": "module",
"scripts": {
@@ -120,7 +120,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.2.3",
"@next/bundle-analyzer": "15.3.0",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -135,8 +135,8 @@
"@types/jest": "29.5.12",
"@types/minimist": "1.2.5",
"@types/node": "22.5.4",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/shelljs": "0.8.15",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -156,16 +156,16 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^10",
"next": "15.2.3",
"next": "15.3.0",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.50.0",
"playwright-core": "1.50.0",
"prettier": "3.5.3",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"rimraf": "6.0.1",
"sharp": "0.32.6",
"sharp": "0.34.0",
"shelljs": "0.8.5",
"slash": "3.0.0",
"sort-package-json": "^2.10.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.32.0",
"version": "3.34.0",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -42,8 +42,8 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"payload": "workspace:*"
},
"peerDependencies": {

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import type { FilterQuery } from 'mongoose'
import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload'
import { Types } from 'mongoose'
import { APIError, getLocalizedPaths } from 'payload'
import { APIError, getFieldByPath, getLocalizedPaths } from 'payload'
import { validOperatorSet } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
@@ -138,7 +138,7 @@ export async function buildSearchParam({
throw new APIError(`Collection with the slug ${collectionSlug} was not found.`)
}
const { Model: SubModel } = getCollection({
const { collectionConfig, Model: SubModel } = getCollection({
adapter: payload.db as MongooseAdapter,
collectionSlug,
})
@@ -154,22 +154,72 @@ export async function buildSearchParam({
},
})
const result = await SubModel.find(subQuery, subQueryOptions)
const field = paths[0].field
const select: Record<string, boolean> = {
_id: true,
}
let joinPath: null | string = null
if (field.type === 'join') {
const relationshipField = getFieldByPath({
fields: collectionConfig.flattenedFields,
path: field.on,
})
if (!relationshipField) {
throw new APIError('Relationship field was not found')
}
let path = relationshipField.localizedPath
if (relationshipField.pathHasLocalized && payload.config.localization) {
path = path.replace('<locale>', locale || payload.config.localization.defaultLocale)
}
select[path] = true
joinPath = path
}
if (joinPath) {
select[joinPath] = true
}
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
const $in: unknown[] = []
result.forEach((doc) => {
const stringID = doc._id.toString()
$in.push(stringID)
result.forEach((doc: any) => {
if (joinPath) {
let ref = doc
if (Types.ObjectId.isValid(stringID)) {
$in.push(doc._id)
for (const segment of joinPath.split('.')) {
if (typeof ref === 'object' && ref) {
ref = ref[segment]
}
}
if (Array.isArray(ref)) {
for (const item of ref) {
if (item instanceof Types.ObjectId) {
$in.push(item)
}
}
} else if (ref instanceof Types.ObjectId) {
$in.push(ref)
}
} else {
const stringID = doc._id.toString()
$in.push(stringID)
if (Types.ObjectId.isValid(stringID)) {
$in.push(doc._id)
}
}
})
if (pathsToQuery.length === 1) {
return {
path,
path: joinPath ? '_id' : path,
value: { $in },
}
}

View File

@@ -81,7 +81,19 @@ export async function parseParams({
[searchParam.path]: searchParam.value,
})
} else {
result[searchParam.path] = searchParam.value
if (result[searchParam.path]) {
if (!result.$and) {
result.$and = []
}
result.$and.push({ [searchParam.path]: result[searchParam.path] })
result.$and.push({
[searchParam.path]: searchParam.value,
})
delete result[searchParam.path]
} else {
result[searchParam.path] = searchParam.value
}
}
} else if (typeof searchParam?.value === 'object') {
result = deepMergeWithCombinedArrays(result, searchParam.value ?? {}, {

View File

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

View File

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

View File

@@ -59,6 +59,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
docToDelete = await db.query[tableName].findFirst(findManyArgs)
}
if (!docToDelete) {
return null
}
const result =
returning === false
? null
@@ -68,6 +72,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
data: docToDelete,
fields: collection.flattenedFields,
joinQuery: false,
tableName,
})
await this.deleteWhere({

View File

@@ -158,6 +158,7 @@ export const findMany = async function find({
data,
fields,
joinQuery,
tableName,
})
})

View File

@@ -196,7 +196,8 @@ export const traverseFields = ({
}
}
currentArgs.with[`${path}${field.name}`] = withArray
const relationName = field.dbName ? `_${arrayTableName}` : `${path}${field.name}`
currentArgs.with[relationName] = withArray
traverseFields({
_locales: withArray.with._locales,

View File

@@ -23,8 +23,10 @@ export { migrateFresh } from './migrateFresh.js'
export { migrateRefresh } from './migrateRefresh.js'
export { migrateReset } from './migrateReset.js'
export { migrateStatus } from './migrateStatus.js'
export { default as buildQuery } from './queries/buildQuery.js'
export { operatorMap } from './queries/operatorMap.js'
export type { Operators } from './queries/operatorMap.js'
export { parseParams } from './queries/parseParams.js'
export { queryDrafts } from './queryDrafts.js'
export { buildDrizzleRelations } from './schema/buildDrizzleRelations.js'
export { buildRawSchema } from './schema/buildRawSchema.js'

View File

@@ -50,7 +50,8 @@ export async function migrateDown(this: DrizzleAdapter): Promise<void> {
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
})
const tableExists = await migrationTableExists(this)
const tableExists = await migrationTableExists(this, db)
if (tableExists) {
await payload.delete({
id: migration.id,

View File

@@ -54,7 +54,7 @@ export async function migrateRefresh(this: DrizzleAdapter) {
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
})
const tableExists = await migrationTableExists(this)
const tableExists = await migrationTableExists(this, db)
if (tableExists) {
await payload.delete({
collection: 'payload-migrations',

View File

@@ -45,7 +45,7 @@ export async function migrateReset(this: DrizzleAdapter): Promise<void> {
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
})
const tableExists = await migrationTableExists(this)
const tableExists = await migrationTableExists(this, db)
if (tableExists) {
await payload.delete({
id: migration.id,

View File

@@ -1,10 +1,16 @@
import type { SQL } from 'drizzle-orm'
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'payload'
import type {
FlattenedBlock,
FlattenedField,
NumberField,
RelationshipField,
TextField,
} from 'payload'
import { and, eq, like, sql } from 'drizzle-orm'
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
import { APIError } from 'payload'
import { APIError, getFieldByPath } from 'payload'
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import { validate as uuidValidate } from 'uuid'
@@ -338,6 +344,112 @@ export const getTableColumnFromPath = ({
})
}
case 'join': {
if (Array.isArray(field.collection)) {
throw new APIError('Not supported')
}
const newCollectionPath = pathSegments.slice(1).join('.')
if (field.hasMany) {
const relationTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}`
const { newAliasTable: aliasRelationshipTable } = getTableAlias({
adapter,
tableName: relationTableName,
})
const relationshipField = getFieldByPath({
fields: adapter.payload.collections[field.collection].config.flattenedFields,
path: field.on,
})
if (!relationshipField) {
throw new APIError('Relationship was not found')
}
addJoinTable({
condition: and(
eq(
adapter.tables[rootTableName].id,
aliasRelationshipTable[
`${(relationshipField.field as RelationshipField).relationTo as string}ID`
],
),
like(aliasRelationshipTable.path, field.on),
),
joins,
queryPath: field.on,
table: aliasRelationshipTable,
})
const relationshipConfig = adapter.payload.collections[field.collection].config
const relationshipTableName = adapter.tableNameMap.get(
toSnakeCase(relationshipConfig.slug),
)
// parent to relationship join table
const relationshipFields = relationshipConfig.flattenedFields
const { newAliasTable: relationshipTable } = getTableAlias({
adapter,
tableName: relationshipTableName,
})
joins.push({
condition: eq(aliasRelationshipTable.parent, relationshipTable.id),
table: relationshipTable,
})
return getTableColumnFromPath({
adapter,
aliasTable: relationshipTable,
collectionPath: newCollectionPath,
constraints,
// relationshipFields are fields from a different collection => no parentIsLocalized
fields: relationshipFields,
joins,
locale,
parentIsLocalized: false,
pathSegments: pathSegments.slice(1),
rootTableName: relationshipTableName,
selectFields,
selectLocale,
tableName: relationshipTableName,
value,
})
}
const newTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[field.collection].config.slug),
)
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
joins.push({
condition: eq(
newAliasTable[field.on.replaceAll('.', '_')],
aliasTable ? aliasTable.id : adapter.tables[tableName].id,
),
table: newAliasTable,
})
return getTableColumnFromPath({
adapter,
aliasTable: newAliasTable,
collectionPath: newCollectionPath,
constraintPath: '',
constraints,
fields: adapter.payload.collections[field.collection].config.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
selectFields,
tableName: newTableName,
value,
})
break
}
case 'number':
case 'text': {
if (field.hasMany) {
@@ -381,7 +493,6 @@ export const getTableColumnFromPath = ({
}
break
}
case 'relationship':
case 'upload': {
const newCollectionPath = pathSegments.slice(1).join('.')
@@ -645,6 +756,7 @@ export const getTableColumnFromPath = ({
value,
})
}
break
}

View File

@@ -19,7 +19,7 @@ type Args = {
aliasTable?: Table
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale: string
locale?: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
selectLocale?: boolean

View File

@@ -2,6 +2,7 @@ import type { CompoundIndex, FlattenedField } from 'payload'
import { InvalidConfiguration } from 'payload'
import {
array,
fieldAffectsData,
fieldIsVirtual,
fieldShouldBeLocalized,
@@ -287,7 +288,9 @@ export const traverseFields = ({
}
}
relationsToBuild.set(fieldName, {
const relationName = field.dbName ? `_${arrayTableName}` : fieldName
relationsToBuild.set(relationName, {
type: 'many',
// arrays have their own localized table, independent of the base table.
localized: false,
@@ -304,7 +307,7 @@ export const traverseFields = ({
},
],
references: ['id'],
relationName: fieldName,
relationName,
to: parentTableName,
},
}

View File

@@ -15,6 +15,7 @@ type TransformArgs = {
joinQuery?: JoinQuery
locale?: string
parentIsLocalized?: boolean
tableName: string
}
// This is the entry point to transform Drizzle output data
@@ -26,6 +27,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
fields,
joinQuery,
parentIsLocalized,
tableName,
}: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
@@ -53,6 +55,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
adapter,
blocks,
config,
currentTableName: tableName,
dataRef: {
id: data.id,
},
@@ -65,7 +68,9 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
path: '',
relationships,
table: data,
tablePath: '',
texts,
topLevelTableName: tableName,
})
deletions.forEach((deletion) => deletion())

View File

@@ -1,6 +1,7 @@
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
@@ -22,6 +23,7 @@ type TraverseFieldsArgs = {
* The full Payload config
*/
config: SanitizedConfig
currentTableName: string
/**
* The data reference to be mutated within this recursive function
*/
@@ -59,10 +61,12 @@ type TraverseFieldsArgs = {
* Data structure representing the nearest table from db
*/
table: Record<string, unknown>
tablePath: string
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
topLevelTableName: string
/**
* Set to a locale if this group of fields is within a localized array or block.
*/
@@ -75,6 +79,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName,
dataRef,
deletions,
fieldPrefix,
@@ -85,7 +90,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path,
relationships,
table,
tablePath,
texts,
topLevelTableName,
withinArrayOrBlockLocale,
}: TraverseFieldsArgs): T => {
const sanitizedPath = path ? `${path}.` : path
@@ -110,6 +117,14 @@ export const traverseFields = <T extends Record<string, unknown>>({
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
if (field.type === 'array') {
const arrayTableName = adapter.tableNameMap.get(
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
)
if (field.dbName) {
fieldData = table[`_${arrayTableName}`]
}
if (Array.isArray(fieldData)) {
if (isLocalized) {
result[field.name] = fieldData.reduce((arrayResult, row) => {
@@ -129,6 +144,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName: arrayTableName,
dataRef: data,
deletions,
fieldPrefix: '',
@@ -138,7 +154,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale: locale,
})
@@ -175,6 +193,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName: arrayTableName,
dataRef: row,
deletions,
fieldPrefix: '',
@@ -184,7 +203,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale,
}),
)
@@ -228,11 +249,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
(block) => typeof block !== 'string' && block.slug === row.blockType,
) as FlattenedBlock | undefined)
const tableName = adapter.tableNameMap.get(
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
)
if (block) {
const blockResult = traverseFields<T>({
adapter,
blocks,
config,
currentTableName: tableName,
dataRef: row,
deletions,
fieldPrefix: '',
@@ -242,7 +268,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale: locale,
})
@@ -300,11 +328,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
delete row._index
}
const tableName = adapter.tableNameMap.get(
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
)
acc.push(
traverseFields<T>({
adapter,
blocks,
config,
currentTableName: tableName,
dataRef: row,
deletions,
fieldPrefix: '',
@@ -314,7 +347,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale,
}),
)
@@ -614,6 +649,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName,
dataRef: groupData as Record<string, unknown>,
deletions,
fieldPrefix: groupFieldPrefix,
@@ -624,7 +660,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${sanitizedPath}${field.name}`,
relationships,
table,
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
texts,
topLevelTableName,
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
})

View File

@@ -423,6 +423,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
path: fieldName,
},
],
req,
},
req?.t,
)
@@ -466,6 +467,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
data: doc,
fields,
joinQuery: false,
tableName,
})
return result

View File

@@ -131,7 +131,7 @@ export const createSchemaGenerator = ({
let foreignKeyDeclaration = `${sanitizeObjectKey(key)}: foreignKey({
columns: [${foreignKey.columns.map((col) => `columns['${col}']`).join(', ')}],
foreignColumns: [${foreignKey.foreignColumns.map((col) => `${accessProperty(col.table, col.name)}`).join(', ')}],
name: '${foreignKey.name}'
name: '${foreignKey.name}'
})`
if (foreignKey.onDelete) {
@@ -167,11 +167,11 @@ ${Object.entries(table.columns)
}${
extrasDeclarations.length
? `, (columns) => ({
${extrasDeclarations.join('\n ')}
${extrasDeclarations.join('\n ')}
})`
: ''
}
)
)
`
tableDeclarations.push(tableCode)
@@ -250,7 +250,7 @@ type DatabaseSchema = {
`
const finalDeclaration = `
declare module '${this.packageName}/types' {
declare module '${this.packageName}' {
export interface GeneratedDatabaseSchema {
schema: DatabaseSchema
}

View File

@@ -1,6 +1,11 @@
import type { DrizzleAdapter } from '../types.js'
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boolean> => {
import type { DrizzleAdapter, PostgresDB } from '../types.js'
export const migrationTableExists = async (
adapter: DrizzleAdapter,
db?: LibSQLDatabase | PostgresDB,
): Promise<boolean> => {
let statement
if (adapter.name === 'postgres') {
@@ -20,7 +25,7 @@ export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boo
}
const result = await adapter.execute({
drizzle: adapter.drizzle,
drizzle: db ?? adapter.drizzle,
raw: statement,
})

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLObjectType } from 'graphql'
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'
export const buildPaginatedListType = (name, docType) =>
new GraphQLObjectType({
name,
fields: {
docs: {
type: new GraphQLList(docType),
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(docType))),
},
hasNextPage: { type: GraphQLBoolean },
hasPrevPage: { type: GraphQLBoolean },
limit: { type: GraphQLInt },
nextPage: { type: GraphQLInt },
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
hasPrevPage: { type: new GraphQLNonNull(GraphQLBoolean) },
limit: { type: new GraphQLNonNull(GraphQLInt) },
nextPage: { type: new GraphQLNonNull(GraphQLInt) },
offset: { type: GraphQLInt },
page: { type: GraphQLInt },
pagingCounter: { type: GraphQLInt },
prevPage: { type: GraphQLInt },
totalDocs: { type: GraphQLInt },
totalPages: { type: GraphQLInt },
page: { type: new GraphQLNonNull(GraphQLInt) },
pagingCounter: { type: new GraphQLNonNull(GraphQLInt) },
prevPage: { type: new GraphQLNonNull(GraphQLInt) },
totalDocs: { type: new GraphQLNonNull(GraphQLInt) },
totalPages: { type: new GraphQLNonNull(GraphQLInt) },
},
})

View File

@@ -348,11 +348,15 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
name: joinName,
fields: {
docs: {
type: Array.isArray(field.collection)
? GraphQLJSON
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
type: new GraphQLNonNull(
Array.isArray(field.collection)
? GraphQLJSON
: new GraphQLList(
new GraphQLNonNull(graphqlResult.collections[field.collection].graphQL.type),
),
),
},
hasNextPage: { type: GraphQLBoolean },
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
},
}),
args: {
@@ -428,7 +432,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
...objectTypeConfig,
[formatName(field.name)]: formattedNameResolver({
type: withNullableType({
type: field?.hasMany === true ? new GraphQLList(type) : type,
type: field?.hasMany === true ? new GraphQLList(new GraphQLNonNull(type)) : type,
field,
forceNullable,
parentIsLocalized,
@@ -856,7 +860,10 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
...objectTypeConfig,
[formatName(field.name)]: formattedNameResolver({
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
type:
field.hasMany === true
? new GraphQLList(new GraphQLNonNull(GraphQLString))
: GraphQLString,
field,
forceNullable,
parentIsLocalized,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.32.0",
"version": "3.34.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
@@ -45,8 +45,8 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.32.0",
"version": "3.34.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.32.0",
"version": "3.34.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.32.0",
"version": "3.34.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -104,16 +104,16 @@
"@babel/preset-env": "7.26.7",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@next/eslint-plugin-next": "15.2.3",
"@next/eslint-plugin-next": "15.3.0",
"@payloadcms/eslint-config": "workspace:*",
"@types/busboy": "1.5.4",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"babel-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"esbuild": "0.24.2",
"esbuild-sass-plugin": "3.3.1",
"eslint-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"payload": "workspace:*",
"swc-plugin-transform-remove-imports": "3.1.0"
},

View File

@@ -1,11 +1,10 @@
'use client'
import type { ClientField } from 'payload'
import { ChevronIcon, Pill, useConfig, useTranslation } from '@payloadcms/ui'
import { ChevronIcon, FieldDiffLabel, Pill, useConfig, useTranslation } from '@payloadcms/ui'
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
import React, { useState } from 'react'
import Label from '../Label/index.js'
import './index.scss'
import { countChangedFields, countChangedFieldsInRows } from '../utilities/countChangedFields.js'
@@ -100,7 +99,7 @@ export const DiffCollapser: React.FC<Props> = ({
return (
<div className={baseClass}>
<Label>
<FieldDiffLabel>
<button
aria-label={isCollapsed ? 'Expand' : 'Collapse'}
className={`${baseClass}__toggle-button`}
@@ -115,7 +114,7 @@ export const DiffCollapser: React.FC<Props> = ({
{t('version:changedFieldsCount', { count: changeCount })}
</Pill>
)}
</Label>
</FieldDiffLabel>
<div className={contentClassNames}>{children}</div>
</div>
)

View File

@@ -1,22 +1,23 @@
import type { I18nClient } from '@payloadcms/translations'
import type {
BaseVersionField,
ClientField,
ClientFieldSchemaMap,
Field,
FieldDiffClientProps,
FieldDiffServerProps,
FieldTypes,
FlattenedBlock,
PayloadComponent,
PayloadRequest,
SanitizedFieldPermissions,
VersionField,
} from 'payload'
import type { DiffMethod } from 'react-diff-viewer-continued'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { dequal } from 'dequal/lite'
import {
type BaseVersionField,
type ClientField,
type ClientFieldSchemaMap,
type Field,
type FieldDiffClientProps,
type FieldDiffServerProps,
type FieldTypes,
type FlattenedBlock,
MissingEditorProp,
type PayloadComponent,
type PayloadRequest,
type SanitizedFieldPermissions,
type VersionField,
} from 'payload'
import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared'
import { diffMethods } from './fields/diffMethods.js'
@@ -238,7 +239,24 @@ const buildVersionField = ({
return null
}
const CustomComponent = field?.admin?.components?.Diff ?? customDiffComponents?.[field.type]
let CustomComponent = customDiffComponents?.[field.type]
if (field?.type === 'richText') {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
if (field.editor.CellComponent) {
CustomComponent = field.editor.DiffComponent
}
}
if (field?.admin?.components?.Diff) {
CustomComponent = field.admin.components.Diff
}
const DefaultComponent = diffComponents?.[field.type]
const baseVersionField: BaseVersionField = {

View File

@@ -7,12 +7,11 @@ import type {
} from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useConfig, useTranslation } from '@payloadcms/ui'
import { FieldDiffLabel, useConfig, useTranslation } from '@payloadcms/ui'
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
import React from 'react'
import ReactDiffViewer from 'react-diff-viewer-continued'
import Label from '../../Label/index.js'
import './index.scss'
import { diffStyles } from '../styles.js'
@@ -169,10 +168,10 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({
return (
<div className={baseClass}>
<Label>
<FieldDiffLabel>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{getTranslation(label, i18n)}
</Label>
</FieldDiffLabel>
<ReactDiffViewer
hideLineNumbers
newValue={versionToRender}

View File

@@ -3,10 +3,9 @@ import type { I18nClient } from '@payloadcms/translations'
import type { Option, SelectField, SelectFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui'
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
import React from 'react'
import Label from '../../Label/index.js'
import './index.scss'
import { diffStyles } from '../styles.js'
import { DiffViewer } from './DiffViewer/index.js'
@@ -103,10 +102,10 @@ export const Select: SelectFieldDiffClientComponent = ({
return (
<div className={baseClass}>
<Label>
<FieldDiffLabel>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{'label' in field && getTranslation(field.label || '', i18n)}
</Label>
</FieldDiffLabel>
<DiffViewer
comparisonToRender={comparisonToRender}
diffMethod={diffMethod}

View File

@@ -2,10 +2,9 @@
import type { TextFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui'
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
import React from 'react'
import Label from '../../Label/index.js'
import './index.scss'
import { diffStyles } from '../styles.js'
import { DiffViewer } from './DiffViewer/index.js'
@@ -34,12 +33,12 @@ export const Text: TextFieldDiffClientComponent = ({
return (
<div className={baseClass}>
<Label>
<FieldDiffLabel>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{'label' in field &&
typeof field.label !== 'function' &&
getTranslation(field.label || '', i18n)}
</Label>
</FieldDiffLabel>
<DiffViewer
comparisonToRender={comparisonToRender}
diffMethod={diffMethod}

View File

@@ -1,4 +1,6 @@
export const diffStyles = {
import type { ReactDiffViewerStylesOverride } from 'react-diff-viewer-continued'
export const diffStyles: ReactDiffViewerStylesOverride = {
diffContainer: {
minWidth: 'unset',
},
@@ -26,4 +28,11 @@ export const diffStyles = {
wordRemovedBackground: 'var(--theme-error-200)',
},
},
wordAdded: {
color: 'var(--theme-success-600)',
},
wordRemoved: {
color: 'var(--theme-error-600)',
textDecorationLine: 'line-through',
},
}

View File

@@ -56,16 +56,6 @@ export const withPayload = (nextConfig = {}, options = {}) => {
...(nextConfig?.outputFileTracingIncludes || {}),
'**/*': [...(nextConfig?.outputFileTracingIncludes?.['**/*'] || []), '@libsql/client'],
},
experimental: {
...(nextConfig?.experimental || {}),
turbo: {
...(nextConfig?.experimental?.turbo || {}),
resolveAlias: {
...(nextConfig?.experimental?.turbo?.resolveAlias || {}),
'payload-mock-package': 'payload-mock-package',
},
},
},
// We disable the poweredByHeader here because we add it manually in the headers function below
...(nextConfig?.poweredByHeader !== false ? { poweredByHeader: false } : {}),
headers: async () => {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.32.0",
"version": "3.34.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -98,7 +98,7 @@
"file-type": "19.3.0",
"get-tsconfig": "4.8.1",
"http-status": "2.1.0",
"image-size": "1.2.0",
"image-size": "2.0.2",
"jose": "5.9.6",
"json-schema-to-typescript": "15.0.3",
"minimist": "1.2.8",
@@ -131,7 +131,7 @@
"graphql-http": "^1.22.0",
"react-datepicker": "7.6.0",
"rimraf": "6.0.1",
"sharp": "0.32.6",
"sharp": "0.34.0",
"typescript-strict-plugin": "2.4.4"
},
"peerDependencies": {

View File

@@ -5,12 +5,17 @@ import type { JSONSchema4 } from 'json-schema'
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
import type { ValidationFieldError } from '../errors/ValidationError.js'
import type { FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
import type {
FieldAffectingData,
RichTextField,
RichTextFieldClient,
Validate,
} from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { RequestContext } from '../index.js'
import type { JsonObject, PayloadRequest, PopulateType } from '../types/index.js'
import type { RichTextFieldClientProps } from './fields/RichText.js'
import type { FieldSchemaMap } from './types.js'
import type { RichTextFieldClientProps, RichTextFieldServerProps } from './fields/RichText.js'
import type { FieldDiffClientProps, FieldDiffServerProps, FieldSchemaMap } from './types.js'
export type AfterReadRichTextHookArgs<
TData extends TypeWithID = any,
@@ -248,7 +253,15 @@ export type RichTextAdapter<
ExtraFieldProperties = any,
> = {
CellComponent: PayloadComponent<never>
FieldComponent: PayloadComponent<never, RichTextFieldClientProps>
/**
* Component that will be displayed in the version diff view.
* If not provided, richtext content will be diffed as JSON.
*/
DiffComponent?: PayloadComponent<
FieldDiffServerProps<RichTextField, RichTextFieldClient>,
FieldDiffClientProps<RichTextFieldClient>
>
FieldComponent: PayloadComponent<RichTextFieldServerProps, RichTextFieldClientProps>
} & RichTextAdapterBase<Value, AdapterProps, ExtraFieldProperties>
export type RichTextAdapterProvider<

View File

@@ -13,8 +13,12 @@ export type Data = {
export type Row = {
blockType?: string
collapsed?: boolean
customComponents?: {
RowLabel?: React.ReactNode
}
id: string
isLoading?: boolean
lastRenderedPath?: string
}
export type FilterOptionsResult = {
@@ -22,6 +26,13 @@ export type FilterOptionsResult = {
}
export type FieldState = {
/**
* This is used to determine if the field was added by the server.
* This ensures the field is not ignored by the client when merging form state.
* This can happen because the current local state is treated as the source of truth.
* See `mergeServerFormState` for more details.
*/
addedByServer?: boolean
customComponents?: {
/**
* This is used by UI fields, as they can have arbitrary components defined if used
@@ -34,7 +45,6 @@ export type FieldState = {
Error?: React.ReactNode
Field?: React.ReactNode
Label?: React.ReactNode
RowLabels?: React.ReactNode[]
}
disableFormData?: boolean
errorMessage?: string
@@ -46,21 +56,17 @@ export type FieldState = {
fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue?: unknown
passesCondition?: boolean
requiresRender?: boolean
rows?: Row[]
/**
* The `serverPropsToIgnore` obj is used to prevent the various properties from being overridden across form state requests.
* This can happen when queueing a form state request with `requiresRender: true` while the another is already processing.
* For example:
* 1. One "add row" action will set `requiresRender: true` and dispatch a form state request
* 2. Another "add row" action will set `requiresRender: true` and queue a form state request
* 3. The first request will return with `requiresRender: false`
* 4. The second request will be dispatched with `requiresRender: false` but should be `true`
* To fix this, only merge the `requiresRender` property if the previous state has not set it to `true`.
* See the `mergeServerFormState` function for implementation details.
* 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,
* what path it was rendered under last.
*
* If this path is undefined, or, if it is different
* from the current path of a given field, the field's components will be re-rendered.
*/
serverPropsToIgnore?: Array<keyof FieldState>
lastRenderedPath?: string
passesCondition?: boolean
rows?: Row[]
valid?: boolean
validate?: Validate
value?: unknown
@@ -95,6 +101,13 @@ export type BuildFormStateArgs = {
*/
language?: keyof SupportedLanguages
locale?: string
/**
* If true, will not render RSCs and instead return a simple string in their place.
* This is useful for environments that lack RSC support, such as Jest.
* Form state can still be built, but any server components will be omitted.
* @default false
*/
mockRSCs?: boolean
operation?: 'create' | 'update'
/*
If true, will render field components within their state object

View File

@@ -99,7 +99,14 @@ export const updateDocument = async <
const password = data?.password
const shouldSaveDraft =
Boolean(draftArg && collectionConfig.versions.drafts) && data._status !== 'published'
const shouldSavePassword = Boolean(password && collectionConfig.auth && !shouldSaveDraft)
const shouldSavePassword = Boolean(
password &&
collectionConfig.auth &&
(!collectionConfig.auth.disableLocalStrategy ||
(typeof collectionConfig.auth.disableLocalStrategy === 'object' &&
collectionConfig.auth.disableLocalStrategy.enableFields)) &&
!shouldSaveDraft,
)
// /////////////////////////////////////
// Handle potentially locked documents

View File

@@ -106,24 +106,26 @@ export const addOrderableFieldsAndHook = (
collection.hooks.beforeChange = []
}
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, operation, req }) => {
// Only set _order on create, not on update (unless explicitly provided)
if (operation === 'create') {
for (const orderableFieldName of orderableFieldNames) {
if (!data[orderableFieldName]) {
const lastDoc = await req.payload.find({
collection: collection.slug,
depth: 0,
limit: 1,
pagination: false,
req,
select: { [orderableFieldName]: true },
sort: `-${orderableFieldName}`,
})
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, originalDoc, req }) => {
for (const orderableFieldName of orderableFieldNames) {
if (!data[orderableFieldName] && !originalDoc?.[orderableFieldName]) {
const lastDoc = await req.payload.find({
collection: collection.slug,
depth: 0,
limit: 1,
pagination: false,
req,
select: { [orderableFieldName]: true },
sort: `-${orderableFieldName}`,
where: {
[orderableFieldName]: {
exists: true,
},
},
})
const lastOrderValue = lastDoc.docs[0]?.[orderableFieldName] || null
data[orderableFieldName] = generateKeyBetween(lastOrderValue, null)
}
const lastOrderValue = lastDoc.docs[0]?.[orderableFieldName] || null
data[orderableFieldName] = generateKeyBetween(lastOrderValue, null)
}
}
@@ -163,7 +165,7 @@ export const addOrderableEndpoint = (config: SanitizedConfig) => {
}
if (
typeof target !== 'object' ||
typeof target.id !== 'string' ||
typeof target.id === 'undefined' ||
typeof target.key !== 'string'
) {
return new Response(JSON.stringify({ error: 'target must be an object with id and key' }), {

View File

@@ -1,6 +1,7 @@
import type { Payload } from '../index.js'
import type { PathToQuery } from './queryValidation/types.js'
import { APIError, type Payload, type SanitizedCollectionConfig } from '../index.js'
// @ts-strict-ignore
import {
type Field,
@@ -151,21 +152,12 @@ export function getLocalizedPaths({
}
switch (matchedField.type) {
case 'json':
case 'richText': {
const upcomingSegments = pathSegments.slice(i + 1).join('.')
lastIncompletePath.complete = true
lastIncompletePath.path = upcomingSegments
? `${currentPath}.${upcomingSegments}`
: currentPath
return paths
}
case 'join':
case 'relationship':
case 'upload': {
// If this is a polymorphic relation,
// We only support querying directly (no nested querying)
if (typeof matchedField.relationTo !== 'string') {
if (matchedField.type !== 'join' && typeof matchedField.relationTo !== 'string') {
const lastSegmentIsValid =
['relationTo', 'value'].includes(pathSegments[pathSegments.length - 1]) ||
pathSegments.length === 1 ||
@@ -188,7 +180,16 @@ export function getLocalizedPaths({
.join('.')
if (nestedPathToQuery) {
const relatedCollection = payload.collections[matchedField.relationTo].config
let relatedCollection: SanitizedCollectionConfig
if (matchedField.type === 'join') {
if (Array.isArray(matchedField.collection)) {
throw new APIError('Not supported')
}
relatedCollection = payload.collections[matchedField.collection].config
} else {
relatedCollection = payload.collections[matchedField.relationTo as string].config
}
const remainingPaths = getLocalizedPaths({
collectionSlug: relatedCollection.slug,
@@ -208,6 +209,15 @@ export function getLocalizedPaths({
break
}
case 'json':
case 'richText': {
const upcomingSegments = pathSegments.slice(i + 1).join('.')
lastIncompletePath.complete = true
lastIncompletePath.path = upcomingSegments
? `${currentPath}.${upcomingSegments}`
: currentPath
return paths
}
default: {
if (i + 1 === pathSegments.length) {

View File

@@ -13,6 +13,7 @@ type Args = {
errors?: { path: string }[]
overrideAccess: boolean
policies?: EntityPolicies
polymorphicJoin?: boolean
req: PayloadRequest
versionFields?: FlattenedField[]
where: Where
@@ -52,6 +53,7 @@ export async function validateQueryPaths({
collections: {},
globals: {},
},
polymorphicJoin,
req,
versionFields,
where,
@@ -77,6 +79,7 @@ export async function validateQueryPaths({
overrideAccess,
path,
policies,
polymorphicJoin,
req,
val,
versionFields,

View File

@@ -21,6 +21,7 @@ type Args = {
parentIsLocalized?: boolean
path: string
policies: EntityPolicies
polymorphicJoin?: boolean
req: PayloadRequest
val: unknown
versionFields?: FlattenedField[]
@@ -39,6 +40,7 @@ export async function validateSearchParam({
parentIsLocalized,
path: incomingPath,
policies,
polymorphicJoin,
req,
val,
versionFields,
@@ -102,6 +104,10 @@ export async function validateSearchParam({
errors.push({ path })
}
if (polymorphicJoin && path === 'relationTo') {
return
}
if (!overrideAccess && fieldAffectsData(field)) {
if (collectionSlug) {
if (!policies.collections[collectionSlug]) {
@@ -140,8 +146,10 @@ export async function validateSearchParam({
const segments = fieldPath.split('.')
let fieldAccess
if (versionFields) {
fieldAccess = policies[entityType][entitySlug]
if (segments[0] === 'parent' || segments[0] === 'version') {
segments.shift()
}

View File

@@ -1,5 +1,6 @@
// @ts-strict-ignore
import type { SanitizedCollectionConfig, SanitizedJoin } from '../collections/config/types.js'
import type { FlattenedField } from '../fields/config/types.js'
import type { JoinQuery, PayloadRequest } from '../types/index.js'
import executeAccess from '../auth/executeAccess.js'
@@ -67,6 +68,7 @@ const sanitizeJoinFieldQuery = async ({
collectionConfig: joinCollectionConfig,
errors,
overrideAccess,
polymorphicJoin: Array.isArray(join.field.collection),
req,
// incoming where input, but we shouldn't validate generated from the access control.
where: joinQuery.where,

View File

@@ -4,6 +4,7 @@ import { en } from '@payloadcms/translations/languages/en'
import { status as httpStatus } from 'http-status'
import type { LabelFunction, StaticLabel } from '../config/types.js'
import type { PayloadRequest } from '../types/index.js'
import { APIError } from './APIError.js'
@@ -28,6 +29,10 @@ export class ValidationError extends APIError<{
errors: ValidationFieldError[]
global?: string
id?: number | string
/**
* req needs to be passed through (if you have one) in order to resolve label functions that may be part of the errors array
*/
req?: Partial<PayloadRequest>
},
t?: TFunction,
) {
@@ -37,8 +42,36 @@ export class ValidationError extends APIError<{
? en.translations.error.followingFieldsInvalid_one
: en.translations.error.followingFieldsInvalid_other
const req = results.req
// delete to avoid logging the whole req
delete results['req']
super(
`${message} ${results.errors.map((f) => f.label || f.path).join(', ')}`,
`${message} ${results.errors
.map((f) => {
if (f.label) {
if (typeof f.label === 'function') {
if (!req || !req.i18n || !req.t) {
return f.path
}
return f.label({ i18n: req.i18n, t: req.t })
}
if (typeof f.label === 'object') {
if (req?.i18n?.language) {
return f.label[req.i18n.language]
}
return f.label[Object.keys(f.label)[0]]
}
return f.label
}
return f.path
})
.join(', ')}`,
httpStatus.BAD_REQUEST,
results,
)

View File

@@ -4,6 +4,8 @@ import type { Config, SanitizedConfig } from '../../config/types.js'
import { APIError } from '../../errors/index.js'
import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
import { flattenAllFields } from '../../utilities/flattenAllFields.js'
import { getFieldByPath } from '../../utilities/getFieldByPath.js'
import { traverseFields } from '../../utilities/traverseFields.js'
import {
fieldShouldBeLocalized,
@@ -74,86 +76,40 @@ export const sanitizeJoinField = ({
if (!joinCollection) {
throw new InvalidFieldJoin(field)
}
let joinRelationship: RelationshipField | UploadField
const pathSegments = field.on.split('.') // Split the schema path into segments
let currentSegmentIndex = 0
let localized = false
// Traverse fields and match based on the schema path
traverseFields({
callback: ({ field, next, parentIsLocalized }) => {
if (!('name' in field) || !field.name) {
return
}
const currentSegment = pathSegments[currentSegmentIndex]
// match field on path segments
if ('name' in field && field.name === currentSegment) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
localized = true
const fieldIndex = currentSegmentIndex
join.getForeignPath = ({ locale }) => {
return pathSegments.reduce((acc, segment, index) => {
let result = `${acc}${segment}`
if (index === fieldIndex) {
result = `${result}.${locale}`
}
if (index !== pathSegments.length - 1) {
result = `${result}.`
}
return result
}, '')
}
}
// Check if this is the last segment in the path
if (
(currentSegmentIndex === pathSegments.length - 1 &&
'type' in field &&
field.type === 'relationship') ||
field.type === 'upload'
) {
joinRelationship = field // Return the matched field
next()
return true
} else {
// Move to the next path segment and continue traversal
currentSegmentIndex++
}
} else {
// skip fields in non-matching path segments
next()
return
}
},
config: config as unknown as SanitizedConfig,
fields: joinCollection.fields,
parentIsLocalized: false,
const relationshipField = getFieldByPath({
fields: flattenAllFields({ cache: true, fields: joinCollection.fields }),
path: field.on,
})
if (!joinRelationship) {
if (
!relationshipField ||
(relationshipField.field.type !== 'relationship' && relationshipField.field.type !== 'upload')
) {
throw new InvalidFieldJoin(join.field)
}
if (!joinRelationship.index && !joinRelationship.unique) {
joinRelationship.index = true
if (relationshipField.pathHasLocalized) {
join.getForeignPath = ({ locale }) => {
return relationshipField.localizedPath.replace('<locale>', locale)
}
}
if (!relationshipField.field.index && !relationshipField.field.unique) {
relationshipField.field.index = true
}
if (validateOnly) {
return
}
join.targetField = joinRelationship
join.targetField = relationshipField.field
// override the join field localized property to use whatever the relationship field has
// or if it's nested to a localized array / blocks / tabs / group
field.localized = localized
field.localized = relationshipField.field.localized
// override the join field hasMany property to use whatever the relationship field has
field.hasMany = joinRelationship.hasMany
field.hasMany = relationshipField.field.hasMany
// @ts-expect-error converting JoinField to FlattenedJoinField to track targetField
field.targetField = join.targetField

View File

@@ -57,7 +57,7 @@ import type {
EmailFieldLabelServerComponent,
FieldDescriptionClientProps,
FieldDescriptionServerProps,
FieldDiffClientComponent,
FieldDiffClientProps,
FieldDiffServerProps,
GroupFieldClientProps,
GroupFieldLabelClientComponent,
@@ -326,7 +326,7 @@ type Admin = {
components?: {
Cell?: PayloadComponent<DefaultServerCellComponentProps, DefaultCellComponentProps>
Description?: PayloadComponent<FieldDescriptionServerProps, FieldDescriptionClientProps>
Diff?: PayloadComponent<FieldDiffServerProps, FieldDiffClientComponent>
Diff?: PayloadComponent<FieldDiffServerProps, FieldDiffClientProps>
Field?: PayloadComponent<FieldClientComponent | FieldServerComponent>
/**
* The Filter component has to be a client component

View File

@@ -77,6 +77,7 @@ export const beforeChange = async <T extends JsonObject>({
collection: collection?.slug,
errors,
global: global?.slug,
req,
},
req.t,
)

View File

@@ -1508,5 +1508,5 @@ export { getLatestCollectionVersion } from './versions/getLatestCollectionVersio
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
export { saveVersion } from './versions/saveVersion.js'
export type { SchedulePublishTaskInput } from './versions/schedule/types.js'
export type { TypeWithVersion } from './versions/types.js'
export type { SchedulePublish, TypeWithVersion } from './versions/types.js'
export { deepMergeSimple } from '@payloadcms/translations/utilities'

View File

@@ -1,19 +1,16 @@
// @ts-strict-ignore
import fs from 'fs/promises'
import sizeOfImport from 'image-size'
import { promisify } from 'util'
import { imageSize } from 'image-size'
import { imageSizeFromFile } from 'image-size/fromFile'
import type { PayloadRequest } from '../types/index.js'
import type { ProbedImageSize } from './types.js'
import { temporaryFileTask } from './tempFile.js'
const { imageSize } = sizeOfImport
const imageSizePromise = promisify(imageSize)
export async function getImageSize(file: PayloadRequest['file']): Promise<ProbedImageSize> {
if (file.tempFilePath) {
return imageSizePromise(file.tempFilePath)
return imageSizeFromFile(file.tempFilePath)
}
// Tiff file do not support buffers or streams, so we must write to file first
@@ -22,7 +19,7 @@ export async function getImageSize(file: PayloadRequest['file']): Promise<Probed
const dimensions = await temporaryFileTask(
async (filepath: string) => {
await fs.writeFile(filepath, file.data)
return imageSizePromise(filepath)
return imageSizeFromFile(filepath)
},
{ extension: 'tiff' },
)

View File

@@ -1,5 +1,6 @@
// @ts-strict-ignore
import type { User } from '../../auth/types.js'
import type { Field } from '../../fields/config/types.js'
import type { TaskConfig } from '../../queues/config/types/taskTypes.js'
import type { SchedulePublishTaskInput } from './types.js'
@@ -87,11 +88,15 @@ export const getSchedulePublishTask = ({
name: 'locale',
type: 'text',
},
{
name: 'doc',
type: 'relationship',
relationTo: collections,
},
...(collections.length > 0
? [
{
name: 'doc',
type: 'relationship',
relationTo: collections,
} satisfies Field,
]
: []),
{
name: 'global',
type: 'select',

View File

@@ -8,6 +8,23 @@ export type Autosave = {
interval?: number
}
export type SchedulePublish = {
/**
* Define a date format to use for the time picker.
*
* @example 'hh:mm' will give a 24 hour clock
*
* @default 'h:mm aa' which is a 12 hour clock
*/
timeFormat?: string
/**
* Intervals for the time picker.
*
* @default 5
*/
timeIntervals?: number
}
export type IncomingDrafts = {
/**
* Enable autosave to automatically save progress while documents are edited.
@@ -17,7 +34,7 @@ export type IncomingDrafts = {
/**
* Allow for editors to schedule publish / unpublish events in the future.
*/
schedulePublish?: boolean
schedulePublish?: boolean | SchedulePublish
/**
* Set validate to true to validate draft documents when saved.
*
@@ -35,7 +52,7 @@ export type SanitizedDrafts = {
/**
* Allow for editors to schedule publish / unpublish events in the future.
*/
schedulePublish: boolean
schedulePublish: boolean | SchedulePublish
/**
* Set validate to true to validate draft documents when saved.
*

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.32.0",
"version": "3.34.0",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {
@@ -64,8 +64,8 @@
},
"devDependencies": {
"@types/find-node-modules": "^2.1.2",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -88,11 +88,11 @@ export const getFields = ({ collection, prefix }: Args): Field[] => {
type: 'group',
fields: [
{
...(existingSizeURLField || ({} as any)),
...(existingSizeURLField || {}),
...baseURLField,
},
],
}
} as Field
}),
}

View File

@@ -108,7 +108,7 @@ export const getFields = ({
fields: [
...(adapter.fields || []),
{
...(existingSizeURLField || ({} as any)),
...(existingSizeURLField || {}),
...baseURLField,
hooks: {
afterRead: [
@@ -124,7 +124,7 @@ export const getFields = ({
},
},
],
}
} as Field
}),
}

View File

@@ -15,7 +15,9 @@ export const getAfterDeleteHook = ({
try {
const filesToDelete: string[] = [
doc.filename,
...Object.values(doc?.sizes || []).map((resizedFileData) => resizedFileData?.filename),
...Object.values(doc?.sizes || []).map(
(resizedFileData) => resizedFileData?.filename as string,
),
]
const promises = filesToDelete.map(async (filename) => {

View File

@@ -18,7 +18,7 @@ export const getAfterReadHook =
let url = value
if (disablePayloadAccessControl && filename) {
url = await adapter.generateURL({
url = await adapter.generateURL?.({
collection,
data,
filename,

View File

@@ -29,7 +29,7 @@ export const getBeforeChangeHook =
if (typeof originalDoc.sizes === 'object') {
filesToDelete = filesToDelete.concat(
Object.values(originalDoc?.sizes || []).map(
(resizedFileData) => resizedFileData?.filename,
(resizedFileData) => resizedFileData?.filename as string,
),
)
}

View File

@@ -67,9 +67,6 @@ export const cloudStoragePlugin =
if ('clientUploadContext' in args.params) {
return adapter.staticHandler(req, args)
}
// Otherwise still skip staticHandler
return null
})
}

View File

@@ -89,7 +89,7 @@ export const initClientUploads = <ExtraProps extends Record<string, unknown>, T>
clientProps: {
collectionSlug,
enabled,
extra: extraClientHandlerProps ? extraClientHandlerProps(collection) : undefined,
extra: extraClientHandlerProps ? extraClientHandlerProps(collection!) : undefined,
prefix,
serverHandlerPath,
},

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.32.0",
"version": "3.34.0",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",
@@ -67,8 +67,8 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/escape-html": "^1.0.4",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"payload": "workspace:*"

View File

@@ -36,7 +36,7 @@ export const sendEmail = async (
if (emails && emails.length) {
const formattedEmails: FormattedEmail[] = await Promise.all(
emails.map(async (email: Email): Promise<FormattedEmail | null> => {
emails.map(async (email: Email): Promise<FormattedEmail> => {
const {
bcc: emailBCC,
cc: emailCC,

View File

@@ -23,6 +23,7 @@ export const generateSubmissionCollection = (
},
relationTo: formSlug,
required: true,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
validate: async (value, { req: { payload }, req }) => {
/* Don't run in the client side */
if (!payload) {
@@ -40,7 +41,7 @@ export const generateSubmissionCollection = (
})
return true
} catch (error) {
} catch (_error) {
return 'Cannot create this submission because this form does not exist.'
}
}

View File

@@ -31,7 +31,7 @@ export const DynamicFieldSelector: React.FC<
return null
})
.filter(Boolean)
.filter((field) => field !== null)
setOptions(allNonPaymentFields)
}
}, [fields, getDataByPath])
@@ -40,9 +40,8 @@ export const DynamicFieldSelector: React.FC<
<SelectField
{...props}
field={{
name: props?.field?.name,
options,
...(props.field || {}),
options,
}}
/>
)

View File

@@ -57,11 +57,11 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col
],
})
if (redirect.fields[2].type !== 'row') {
redirect.fields[2].label = 'Custom URL'
if (redirect.fields[2]!.type !== 'row') {
redirect.fields[2]!.label = 'Custom URL'
}
redirect.fields[2].admin = {
redirect.fields[2]!.admin = {
condition: (_, siblingData) => siblingData?.type === 'custom',
}
}

View File

@@ -16,7 +16,7 @@ export const replaceDoubleCurlys = (str: string, variables?: EmailVariables): st
return variables.map(({ field, value }) => `${field} : ${value}`).join(' <br /> ')
} else if (variable === '*:table') {
return keyValuePairToHtmlTable(
variables.reduce((acc, { field, value }) => {
variables.reduce<Record<string, string>>((acc, { field, value }) => {
acc[field] = value
return acc
}, {}),

View File

@@ -106,7 +106,7 @@ export const serializeSlate = (children?: Node[], submissionData?: any): string
`
case 'link':
return `
<a href={${escapeHTML(replaceDoubleCurlys(node.url, submissionData))}}>
<a href={${escapeHTML(replaceDoubleCurlys(node.url!, submissionData))}}>
${serializeSlate(node.children, submissionData)}
</a>
`

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-import-export",
"version": "3.32.0",
"version": "3.34.0",
"description": "Import-Export plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-multi-tenant",
"version": "3.32.0",
"version": "3.34.0",
"description": "Multi Tenant plugin for Payload",
"keywords": [
"payload",

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