Compare commits

...

33 Commits

Author SHA1 Message Date
Sasha
219aebbec0 setup config 2025-04-18 20:00:05 +03:00
Dan Ribbens
df7a3692f7 fix(plugin-search): delete does not also delete the search doc (#12148)
The plugin-search collection uses an `afterDelete` hook to remove search
records from the database. Since a deleted document in postgres causes
cascade updates for the foreign key, the query for the document by
relationship was not returning the record to be deleted.

The solution was to change the delete hook to `beforeDelete` for the
search enabled collections. This way we purge records before the main
document so the search document query can find and delete the record as
expected.

An alternative solution in #9623 would remove the `req` so the delete
query could still find the document, however, this just works outside of
transactions which isn't desirable.

fixes https://github.com/payloadcms/payload/issues/9443
2025-04-18 09:47:36 -04:00
Corey Larson
b750ba4509 fix(ui): reflect default sort in join tables (#12084)
<!--

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 ensures defaultSort is reflected in join tables.

### Why?

Currently, default sort is not reflected in the join table state. The
data _is_ sorted correctly, but the table state sort is undefined. This
is mainly an issue for join fields with `orderable: true` because you
can't re-order the table until `order` is the selected sort column.

### How?

Added `defaultSort` prop to the `<ListQueryProvider />` in the
`<RelationshipTable />` and ensured the default state gets set in
`<ListQueryProvider />` when `modifySearchParams` is false.

**Before:**

<img width="1390" alt="Screenshot 2025-04-11 at 2 33 19 AM"
src="https://github.com/user-attachments/assets/4a008d98-d308-4397-a35a-69795e5a6070"
/>

**After:**

<img width="1362" alt="Screenshot 2025-04-11 at 3 04 07 AM"
src="https://github.com/user-attachments/assets/4748e354-36e4-451f-83e8-6f84fe58d5b5"
/>

Fixes #12083

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-04-18 07:10:48 -03:00
Patrik
d55306980e feat: adds beforeDocumentControls slot to allow custom component injection next to document controls (#12104)
### What

This PR introduces a new `beforeDocumentControls` slot to the edit view
of both collections and globals.

It allows injecting one or more custom components next to the document
control buttons (e.g., Save, Publish, Save Draft) in the admin UI —
useful for adding context, additional buttons, or custom UI elements.

#### Usage

##### For collections: 

```
admin: {
  components: {
    edit: {
      beforeDocumentControls: ['/path/to/CustomComponent'],
    },
  },
},
```

##### For globals:

```
admin: {
  components: {
    elements: {
      beforeDocumentControls: ['/path/to/CustomComponent'],
    },
  },
},
```
2025-04-17 15:23:17 -04:00
Patrik
34ea6ec14f feat: adds showSaveDraftButton option to show draft button with autosave enabled (#12150)
This adds a new `showSaveDraftButton` option to the
`versions.drafts.autosave` config for collections and globals.

By default, the "Save as draft" button is hidden when autosave is
enabled. This new option allows the button to remain visible for manual
saves while autosave is active.

Also updates the admin UI logic to conditionally render the button when
this flag is set, and updates the documentation with an example usage.
2025-04-17 14:45:10 -04:00
Elliot DeNolf
17d5168728 chore(release): v3.35.1 [skip ci] 2025-04-17 11:02:39 -04:00
Jessica Chowdhury
ed50a79643 fix(next): missing @payloadcms/next/auth export (#12144)
Follow up to #11900. The `@payloadcms/next/auth` export was missing from
the published package.json because it was excluded from the
`publishConfig` property.
2025-04-17 10:55:11 -04:00
Sasha
0a59707ea0 chore(db-postgres): improve table name length exceeded error message (#12142)
Improves the error message when table name length exceeds 63 characters
with the tip that you can use the `dbName` property.
2025-04-17 13:55:12 +00:00
Elliot DeNolf
bcbb912d50 chore(release): v3.35.0 [skip ci] 2025-04-16 15:52:57 -04:00
Sasha
1c99f46e4f feat: queriable / sortable / useAsTitle virtual fields linked with a relationship field (#11805)
This PR adds an ability to specify a virtual field in this way
```js
{
  slug: 'posts',
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
  ],
},
{
  slug: 'virtual-relations',
  fields: [
    {
      name: 'postTitle',
      type: 'text',
      virtual: 'post.title',
    },
    {
      name: 'post',
      type: 'relationship',
      relationTo: 'posts',
    },
  ],
},
```

Then, every time you query `virtual-relations`, `postTitle` will be
automatically populated (even if using `depth: 0`) on the db level. This
field also, unlike `virtual: true` is available for querying / sorting /
`useAsTitle`.

Also, the field can be deeply nested to 2 or more relationships, for
example:
```
{
  name: 'postCategoryTitle',
  type: 'text',
  virtual: 'post.category.title',
},
```

Where the current collection has `post` - a relationship to `posts`, the
collection `posts` has `category` that's a relationship to `categories`
and finally `categories` has `title`.
2025-04-16 15:46:18 -04:00
Patrik
c877b1ad43 feat: threads operation through field condition function (#12132)
This PR updates the field `condition` function property to include a new
`operation` argument.

The `operation` arg provides a string relating to which operation the
field type is currently executing within.

#### Changes:

- Added `operation: Operation` in the Condition type.
- Updated relevant condition checks to ensure correct parameter usage.
2025-04-16 15:38:53 -04:00
Philipp Schneider
4426625b83 perf(ui): prevent blockType: "$undefined" from being sent through the network (#12131)
Removes `$undefined` strings from being sent through the network when
sending form state requests. When adding new array rows, we assign
`blockType: undefined` which is stringified to `"$undefined"`. This is
unnecessary, as simply not sending this property is equivalent, and this
is only a requirement for blocks. This change will save on request size,
albeit minimal.

| Before | After |
|--|--|
|<img width="1267" alt="Untitled"
src="https://github.com/user-attachments/assets/699f38bd-7db9-4a52-931d-084b8af8530f"
/> | <img width="1285" alt="image"
src="https://github.com/user-attachments/assets/986ecd4c-f22d-4143-ad38-0c5f52439c67"
/> |
2025-04-16 15:03:35 -04:00
Tylan Davis
23628996d0 chore: adjusts ChevronIcon styling to match other icons (#12133)
### What?

Adjusts the `ChevronIcon` component to match the sizing of other icons
in the `ui` package. Also adds various styling adjustments to places
where icons are used.

### Why?

Using the `ChevronIcon` in other elements currently requires different
styling to make it consistent with other icons. This will make it so
that any usage of the any icons is consistent across components.

### How?

Resizes the `ChevronIcon` components and updates styling throughout the
admin panel.
2025-04-16 17:24:10 +00:00
Kristian Djaković
b9832f40e4 docs: fix syntax issue in blocks field (#11855)
<!--

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

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

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

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

-->

### What?

This PR fixes the config example in the block field page.

### Why?

The syntax was incorrect

### How?

Missing object property
2025-04-16 10:27:42 -04:00
Jacob Fletcher
a675c04c99 fix: respects boolean query preset constraints (#12124)
Returning a boolean value from a constraint-level access control
function does nothing. For example:

```ts
{
  label: 'Noone',
  value: 'noone',
  access: () => false,
},
```

This is because we were only handling query objects, disregarding any
boolean values. The fix is to check if the query is a boolean, and if
so, format a query object to return.
2025-04-16 09:16:43 -04:00
James Mikrut
e79b20363e fix: ensures cors headers are run against custom endpoints (#12091)
Restores goal of #10597 and reverts #10718

This is a more surgical way of adding CORS headers to custom endpoints
2025-04-16 09:15:39 -04:00
Jacob Fletcher
21599b87f5 fix(ui): stale paths on custom components within rows (#11973)
When server rendering custom components within form state, those
components receive a path that is correct at render time, but
potentially stale after manipulating array and blocks rows. This causes
the field to briefly render incorrect values while the form state
request is in flight.

The reason for this is that paths are passed as a prop statically into
those components. Then when we manipulate rows, form state is modified,
potentially changing field paths. The component's `path` prop, however,
hasn't changed. This means it temporarily points to the wrong field in
form state, rendering the data of another row until the server responds
with a freshly rendered component.

This is not an issue with default Payload fields as they are rendered on
the client and can be passed dynamic props.

This is only an issue within custom server components, including rich
text fields which are treated as custom components. Since they are
rendered on the server and passed to the client, props are inaccessible
after render.

The fix for this is to provide paths dynamically through context. This
way as we make changes to form state, there is a mechanism in which
server components can receive the updated path without waiting on its
props to update.
2025-04-15 15:23:51 -04:00
Dan Ribbens
e90ff72b37 fix: reordering draft documents causes data loss (#12109)
Re-ordering documents with drafts uses `payload.update()` with `select:
{ id: true }` and that causes draft versions of those docs to be updated
without any data. I've removed the `select` optimization to prevent data
loss.

Fixes #12097
2025-04-15 12:09:55 -04:00
Tobias Odendahl
babf4f965d fix(richtext-lexical): allow to indent children even if their parents are not indentable (#12042)
### What?
Allows to indent children in richtext-lexical if the parent of that
child is not indentable. Changes the behavior introduced in
https://github.com/payloadcms/payload/pull/11739

### Why?
If there is a document structure with e.g. `tableNode > list > listItem`
and indentation of `tableNode` is disabled, it should still be possible
to indent the list items.

### How?
Disable the indent button only if indentation of one of the selected
nodes itself is disabled.
2025-04-15 09:02:41 -03:00
Dan Ribbens
6572bf4ae1 fix(db-sqlite): text field converts to floating point number (#12107)
### What?

Converts numbers passed to a text field to avoid the database/drizzle
from converting it incorrectly.

### Why?

If you have a hook that passes a value to another field you can
experience this problem where drizzle converts a number value for a text
field to a floating point number in sqlite for example.

### How?

Adds logic to `transform/write/traverseFields.ts` to cast text field
values to string.
2025-04-14 17:05:08 -04:00
Adler Weber
da7be35a15 feat(db-postgres): dependency inject pg to allow Sentry instrumentation (#11478)
### What?

I changed the interface of `@payloadcms/db-postgres` to allow a user to
(optionally) inject their own `pg` module.

### Why?

I noticed that `@payloadcms/sentry-plugin` wasn't instrumenting
Payload's database queries through the [local payload
API](https://payloadcms.com/docs/local-api/overview):


![image](https://github.com/user-attachments/assets/425691f5-cf7e-4625-89e0-6d07dda9cbc0)

This is because Sentry applies a patch to the `pg` driver on import. For
whatever reason, it doesn't patch `pg` when imported by dependencies
(e.g. `@payloadcms/db-postgres`). After applying this fix, I can see the
underlying query traces!


![image](https://github.com/user-attachments/assets/fb6f9aef-13d9-41b1-b4cc-36c565d15930)
2025-04-14 15:27:53 -04:00
Sam Wheeler
55d00e2b1d feat(ui): add option for rendering the relationship field as list drawer (#11553)
### What?

This PR adds the ability to use the ListDrawer component for selecting
related collections for the relationship field instead of the default
drop down interface. This exposes the advanced filtering options that
the list view provides and provides a good interface for searching for
the correct relationship when the workflows may be more complicated.
I've added an additional "selectionType" prop to the relationship field
admin config that defaults to "dropdown" for compatability with the
existing implementation but "drawer" can be passed in as well which
enables using the ListDrawer for selecting related components.

### Why?

Adding the ability to search through the list view enables advanced
workflows or handles edge cases when just using the useAsTitle may not
be informative enough to find the related record that the user wants.
For example, if we have a collection of oscars nominations and are
trying to relate the nomination to the person who recieved the
nomination there may be multiple actors with the same name (Michelle
Williams, for example:
[https://www.imdb.com/name/nm0931329/](https://www.imdb.com/name/nm0931329/),
[https://www.imdb.com/name/nm0931332/](https://www.imdb.com/name/nm0931332/)).
It would be hard to search through the current dropdown ui to choose the
correct person, but in the list view the user could use other fields to
identify the correct person such as an imdb id, description, or anything
else they have in the collection for that person. Other advanced
workflows could be if there are multiple versions of a record in a
collection and the user wants to select the most recent one or just
anything where the user needs to see more details about the record that
they are setting up the relationship to.

### How?

This implementation just re-uses the useListDrawer hook and the
ListDrawer component so the code changes are pretty minimal. The main
change is a new onListSelect handler that gets passed into the
ListDrawer and handles updating the value in the field when a record is
selected in the ListDrawer.

There were also a two things that I didn't implement as they would
require broader code changes 1) Bulk select from the ListDrawer when a
relationship is hasMany - when using bulkSelect in the list drawer the
relatedCollection doesn't get returned so this doesn't work for
polymorphic relationships. Updating this would involve changing the
useListDrawer hook 2) Hide values that are already selected from the
ListDrawer - potentially possible by modifying the filterOptions and
passing in an additional filter but honestly it may not be desired
behaviour to hide values from the ListDrawer as this could be confusing
for the user if they don't see records that they are expected to see
(maybe if anything make them unselectable and indicate that they are
disabled). Currently if an already selected value gets selected the
selected value gets replaced by the new value



https://github.com/user-attachments/assets/fee164da-4270-4612-9304-73ccf34ccf69

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-04-14 14:37:09 -04:00
AoiYamada
5b554e5256 fix(templates): missing default value in select field (#11715)
### What?
The default value is hardcoded instead of respecting the value filled in
the form setting

Fixes #
pass it down from props

Co-authored-by: Pan <kpkong@hk01.com>
2025-04-14 12:38:40 -04:00
Edgar Guerra
85e6edf21e fix(translations): add missing Catalan translations (#10682)
### What?
There are some missing translations in Catalan, both related to the word
Collections, which in Catalan is "Col·leccions".
### Why?
To contribute to the Catalan language as a developer and native speaker
;)
### How?
Updated the wording in the `ca.ts` translations object, also removed
`catalan` from `not implemented languages` comment
2025-04-14 11:21:27 -04:00
Tobias Odendahl
b354d00aa4 feat(ui): use defaultDepth in API view (#11950)
### What?
Respects the defaultDepth setting in the admin UI API view.
 
### Why?
The current default is hardcoded to `1` with no configuration option.
This can lead to performance issues on documents with a lot of related
large documents. Having the ability to define a different default can
prevent this issue.

### How?
Set the depth in the API view to `config.defaultDepth` as default.

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-04-14 10:39:04 -04:00
Jessica Chowdhury
c661d33b13 docs: minor formatting tweaks to server function examples (#12102)
Misc formatting tweaks for server function examples in docs.
2025-04-14 13:05:16 +01:00
Jessica Chowdhury
6b349378e0 feat: adds and exports reusable auth server functions (#11900)
### What
Adds exportable server functions for `login`, `logout` and `refresh`
that are fully typed and ready to use.

### Why
Creating server functions for these auth operations require the
developer to manually set and handle the cookies / auth JWT. This can be
a complex and involved process - instead we want to provide an option
that will handle the cookies internally and simplify the process for the
user.

### How
Three re-usable functions can be exported from
`@payload/next/server-functions`:
- login
- logout
- refresh

Examples of how to use these functions will be added to the docs
shortly, along with more in-depth info on server functions.
2025-04-14 09:47:08 +01:00
Paul
39462bc6b9 chore: assign available port to env variable in dev suite (#12092)
Previously when the port number was bumped up (eg `3001`) in our dev
suite, HMR wouldn't work because it couldn't reliably read the new used
port and it would default to `3000`.

This assigns it properly to the env var and fixes that issues so HMR in
our dev suite works on other ports too.

Testing steps:
- Have a local instance of dev suite running already on port 3000
- New repo run dev, it will bump to `3001`
- Make any config change and you will see that HMR does not work without
this fix
2025-04-11 19:24:24 +00: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
258 changed files with 5684 additions and 1676 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
@@ -310,6 +306,10 @@ jobs:
- 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

View File

@@ -158,7 +158,7 @@ mutation {
```ts
const result = await payload.login({
collection: '[collection-slug]',
collection: 'collection-slug',
data: {
email: 'dev@payloadcms.com',
password: 'get-out',
@@ -166,6 +166,13 @@ const result = await payload.login({
})
```
<Banner type="success">
**Server Functions:** Payload offers a ready-to-use `login` server function
that utilizes the Local API. For integration details and examples, check out
the [Server Function
docs](../local-api/server-functions#reusable-payload-server-functions).
</Banner>
## Logout
As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way.
@@ -189,6 +196,13 @@ mutation {
}
```
<Banner type="success">
**Server Functions:** Payload provides a ready-to-use `logout` server function
that manages the user's cookie for a seamless logout. For integration details
and examples, check out the [Server Function
docs](../local-api/server-functions#reusable-payload-server-functions).
</Banner>
## Refresh
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
@@ -240,6 +254,13 @@ mutation {
}
```
<Banner type="success">
**Server Functions:** Payload exports a ready-to-use `refresh` server function
that automatically renews the user's token and updates the associated cookie.
For integration details and examples, check out the [Server Function
docs](../local-api/server-functions#reusable-payload-server-functions).
</Banner>
## Verify by Email
If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API.
@@ -270,7 +291,7 @@ mutation {
```ts
const result = await payload.verifyEmail({
collection: '[collection-slug]',
collection: 'collection-slug',
token: 'TOKEN_HERE',
})
```
@@ -308,7 +329,7 @@ mutation {
```ts
const result = await payload.unlock({
collection: '[collection-slug]',
collection: 'collection-slug',
})
```
@@ -349,7 +370,7 @@ mutation {
```ts
const token = await payload.forgotPassword({
collection: '[collection-slug]',
collection: 'collection-slug',
data: {
email: 'dev@payloadcms.com',
},

View File

@@ -240,8 +240,8 @@ export default buildConfig({
// highlight-start
cors: {
origins: ['http://localhost:3000'],
headers: ['x-custom-header']
}
headers: ['x-custom-header'],
},
// highlight-end
})
```

View File

@@ -101,14 +101,15 @@ export const MyCollection: CollectionConfig = {
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). |
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `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
@@ -133,13 +134,14 @@ export const MyGlobal: GlobalConfig = {
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). |
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `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
@@ -191,6 +193,73 @@ export function MySaveButton(props: SaveButtonClientProps) {
}
```
### beforeDocumentControls
The `beforeDocumentControls` property allows you to render custom components just before the default document action buttons (like Save, Publish, or Preview). This is useful for injecting custom buttons, status indicators, or any other UI elements before the built-in controls.
To add `beforeDocumentControls` components, use the `components.edit.beforeDocumentControls` property in you [Collection Config](../configuration/collections) or `components.elements.beforeDocumentControls` in your [Global Config](../configuration/globals):
#### Collections
```
export const MyCollection: CollectionConfig = {
admin: {
components: {
edit: {
// highlight-start
beforeDocumentControls: ['/path/to/CustomComponent'],
// highlight-end
},
},
},
}
```
#### Globals
```
export const MyGlobal: GlobalConfig = {
admin: {
components: {
elements: {
// highlight-start
beforeDocumentControls: ['/path/to/CustomComponent'],
// highlight-end
},
},
},
}
```
Here's an example of a custom `beforeDocumentControls` component:
#### Server Component
```tsx
import React from 'react'
import type { BeforeDocumentControlsServerProps } from 'payload'
export function MyCustomDocumentControlButton(
props: BeforeDocumentControlsServerProps,
) {
return <div>This is a custom beforeDocumentControl button (Server)</div>
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { BeforeDocumentControlsClientProps } from 'payload'
export function MyCustomDocumentControlButton(
props: BeforeDocumentControlsClientProps,
) {
return <div>This is a custom beforeDocumentControl button (Client)</div>
}
```
### SaveDraftButton
The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View.

View File

@@ -352,18 +352,20 @@ const config = buildConfig({
},
],
},
{
{
slug: 'collection2',
fields: [
{
name: 'editor',
type: 'richText',
editor: lexicalEditor({
BlocksFeature({
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
blocks: ['TextBlock'],
})
})
features: [
BlocksFeature({
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
blocks: ['TextBlock'],
}),
],
}),
},
],
},

View File

@@ -541,6 +541,7 @@ The `ctx` object:
| Property | Description |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
| **`operation`** | A string relating to which operation the field type is currently executing within. |
| **`path`** | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
| **`user`** | The currently authenticated user object. |

View File

@@ -94,6 +94,7 @@ The Relationship Field inherits all of the default options from the base [Field
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
| **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. |
### Sort Options

View File

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

View File

@@ -310,7 +310,171 @@ export const PostForm: React.FC = () => {
## Reusable Payload Server Functions
Coming soon…
Managing authentication with the Local API can be tricky as you have to handle cookies and tokens yourself, and there aren't built-in logout or refresh functions since these only modify cookies. To make this easier, we provide `login`, `logout`, and `refresh` as ready-to-use server functions. They take care of the underlying complexity so you don't have to.
### Login
Logs in a user by verifying credentials and setting the authentication cookie. This function allows login via username or email, depending on the collection auth configuration.
#### Importing the `login` function
```ts
import { login } from '@payloadcms/next/auth'
```
The login function needs your Payload config, which cannot be imported in a client component. To work around this, create a simple server function like the one below, and call it from your client.
```ts
'use server'
import { login } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function loginAction({
email,
password,
}: {
email: string
password: string
}) {
try {
const result = await login({
collection: 'users',
config,
email,
password,
})
return result
} catch (error) {
throw new Error(
`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
```
#### Login from the React Client Component
```tsx
'use client'
import { useState } from 'react'
import { loginAction } from '../loginAction'
export default function LoginForm() {
const [email, setEmail] = useState<string>('')
const [password, setPassword] = useState<string>('')
return (
<form onSubmit={() => loginAction({ email, password })}>
<label htmlFor="email">Email</label>
<input
id="email"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEmail(e.target.value)
}
type="email"
value={email}
/>
<label htmlFor="password">Password</label>
<input
id="password"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
type="password"
value={password}
/>
<button type="submit">Login</button>
</form>
)
}
```
### Logout
Logs out the current user by clearing the authentication cookie.
#### Importing the `logout` function
```ts
import { logout } from '@payloadcms/next/auth'
```
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below.
```ts
'use server'
import { logout } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function logoutAction() {
try {
return await logout({ config })
} catch (error) {
throw new Error(
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
```
#### Logout from the React Client Component
```tsx
'use client'
import { logoutAction } from '../logoutAction'
export default function LogoutButton() {
return <button onClick={() => logoutFunction()}>Logout</button>
}
```
### Refresh
Refreshes the authentication token for the logged-in user.
#### Importing the `refresh` function
```ts
import { refresh } from '@payloadcms/next/auth'
```
As with login and logout, you need to pass your Payload config to this function. Create a helper server function like the one below. Passing the config directly to the client is not possible and will throw errors.
```ts
'use server'
import { refresh } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function refreshAction() {
try {
return await refresh({
collection: 'users', // pass your collection slug
config,
})
} catch (error) {
throw new Error(
`Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
```
#### Using Refresh from the React Client Component
```tsx
'use client'
import { refreshAction } from '../actions/refreshAction'
export default function RefreshTokenButton() {
return <button onClick={() => refreshFunction()}>Refresh</button>
}
```
## Error Handling in Server Functions

View File

@@ -22,6 +22,7 @@ Collections and Globals both support the same options for configuring autosave.
| Drafts Autosave Options | Description |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. |
| `showSaveDraftButton` | Set this to `true` to show the "Save as draft" button even while autosave is enabled. Defaults to `false`. |
**Example config with versions, drafts, and autosave enabled:**
@@ -50,9 +51,13 @@ export const Pages: CollectionConfig = {
drafts: {
autosave: true,
// Alternatively, you can specify an `interval`:
// Alternatively, you can specify an object to customize autosave:
// autosave: {
// Define how often the document should be autosaved (in milliseconds)
// interval: 1500,
//
// Show the "Save as draft" button even while autosave is enabled
// showSaveDraftButton: true,
// },
},
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import type { Connect, Migration, Payload } from 'payload'
import { pushDevSchema } from '@payloadcms/drizzle'
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
import type { PostgresAdapter } from './types.js'
@@ -61,7 +60,7 @@ export const connect: Connect = async function connect(
try {
if (!this.pool) {
this.pool = new pg.Pool(this.poolOptions)
this.pool = new this.pg.Pool(this.poolOptions)
await connectWithReconnect({ adapter: this, payload: this.payload })
}

View File

@@ -54,6 +54,7 @@ import {
} from '@payloadcms/drizzle/postgres'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
import pgDependency from 'pg'
import { fileURLToPath } from 'url'
import type { Args, PostgresAdapter } from './types.js'
@@ -130,6 +131,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
operators: operatorMap,
pg: args.pg || pgDependency,
pgSchema: adapterSchema,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
pool: undefined,

View File

@@ -12,6 +12,8 @@ import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
import type { Pool, PoolConfig } from 'pg'
type PgDependency = typeof import('pg')
export type Args = {
/**
* Transform the schema after it's built.
@@ -45,6 +47,7 @@ export type Args = {
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
pg?: PgDependency
pool: PoolConfig
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
@@ -74,6 +77,7 @@ type ResolveSchemaType<T> = 'schema' extends keyof T
type Drizzle = NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
export type PostgresAdapter = {
drizzle: Drizzle
pg: PgDependency
pool: Pool
poolOptions: PoolConfig
} & BasePostgresAdapter
@@ -98,6 +102,8 @@ declare module 'payload' {
initializing: Promise<void>
localesSuffix?: string
logger: DrizzleConfig['logger']
/** Optionally inject your own node-postgres. This is required if you wish to instrument the driver with @payloadcms/plugin-sentry. */
pg?: PgDependency
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,9 @@ export const createTableName = ({
if (result.length > 63) {
throw new APIError(
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}.
Tip: You can use the dbName property to reduce the table name length.
`,
)
}

View File

@@ -496,6 +496,10 @@ export const traverseFields = ({
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
}
if (field.type === 'text' && value && typeof value !== 'string') {
formattedValue = JSON.stringify(value)
}
if (field.type === 'date') {
if (typeof value === 'number' && !Number.isNaN(value)) {
formattedValue = new Date(value).toISOString()

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,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.34.0",
"version": "3.35.1",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.34.0",
"version": "3.35.1",
"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.34.0",
"version": "3.35.1",
"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.34.0",
"version": "3.35.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -37,6 +37,11 @@
"types": "./src/exports/routes.ts",
"default": "./src/exports/routes.ts"
},
"./auth": {
"import": "./src/exports/auth.ts",
"types": "./src/exports/auth.ts",
"default": "./src/exports/auth.ts"
},
"./templates": {
"import": "./src/exports/templates.ts",
"types": "./src/exports/templates.ts",
@@ -151,6 +156,11 @@
"types": "./dist/exports/templates.d.ts",
"default": "./dist/exports/templates.js"
},
"./auth": {
"import": "./dist/exports/auth.js",
"types": "./dist/exports/auth.d.ts",
"default": "./dist/exports/auth.js"
},
"./utilities": {
"import": "./dist/exports/utilities.js",
"types": "./dist/exports/utilities.d.ts",

View File

@@ -0,0 +1,87 @@
'use server'
import type { CollectionSlug } from 'payload'
import { cookies as getCookies } from 'next/headers.js'
import { generatePayloadCookie, getPayload } from 'payload'
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'
type LoginWithEmail = {
collection: CollectionSlug
config: any
email: string
password: string
username?: never
}
type LoginWithUsername = {
collection: CollectionSlug
config: any
email?: never
password: string
username: string
}
type LoginArgs = LoginWithEmail | LoginWithUsername
export async function login({ collection, config, email, password, username }: LoginArgs): Promise<{
token?: string
user: any
}> {
const payload = await getPayload({ config })
const authConfig = payload.collections[collection]?.config.auth
if (!authConfig) {
throw new Error(`No auth config found for collection: ${collection}`)
}
const loginWithUsername = authConfig?.loginWithUsername ?? false
if (loginWithUsername) {
if (loginWithUsername.allowEmailLogin) {
if (!email && !username) {
throw new Error('Email or username is required.')
}
} else {
if (!username) {
throw new Error('Username is required.')
}
}
} else {
if (!email) {
throw new Error('Email is required.')
}
}
let loginData
if (loginWithUsername) {
loginData = username ? { password, username } : { email, password }
} else {
loginData = { email, password }
}
try {
const result = await payload.login({
collection,
data: loginData,
})
if (result.token) {
await setPayloadAuthCookie({
authConfig,
cookiePrefix: payload.config.cookiePrefix,
token: result.token,
})
}
if ('removeTokenFromResponses' in config && config.removeTokenFromResponses) {
delete result.token
}
return result
} catch (e) {
console.error('Login error:', e)
throw new Error(`${e}`)
}
}

View File

@@ -0,0 +1,29 @@
'use server'
import { cookies as getCookies, headers as nextHeaders } from 'next/headers.js'
import { getPayload } from 'payload'
import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
export async function logout({ config }: { config: any }) {
try {
const payload = await getPayload({ config })
const headers = await nextHeaders()
const result = await payload.auth({ headers })
if (!result.user) {
return { message: 'User already logged out', success: true }
}
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
if (existingCookie) {
const cookies = await getCookies()
cookies.delete(existingCookie.name)
return { message: 'User logged out successfully', success: true }
}
} catch (e) {
console.error('Logout error:', e)
throw new Error(`${e}`)
}
}

View File

@@ -0,0 +1,42 @@
'use server'
import type { CollectionSlug } from 'payload'
import { headers as nextHeaders } from 'next/headers.js'
import { getPayload } from 'payload'
import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'
export async function refresh({ collection, config }: { collection: CollectionSlug; config: any }) {
try {
const payload = await getPayload({ config })
const authConfig = payload.collections[collection]?.config.auth
if (!authConfig) {
throw new Error(`No auth config found for collection: ${collection}`)
}
const { user } = await payload.auth({ headers: await nextHeaders() })
if (!user) {
throw new Error('User not authenticated')
}
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
if (!existingCookie) {
return { message: 'No valid token found', success: false }
}
await setPayloadAuthCookie({
authConfig,
cookiePrefix: payload.config.cookiePrefix,
token: existingCookie.value,
})
return { message: 'Token refreshed successfully', success: true }
} catch (e) {
console.error('Refresh error:', e)
throw new Error(`${e}`)
}
}

View File

@@ -0,0 +1,3 @@
export { login } from '../auth/login.js'
export { logout } from '../auth/logout.js'
export { refresh } from '../auth/refresh.js'

View File

@@ -0,0 +1,10 @@
import { cookies as getCookies } from 'next/headers.js'
type Cookie = {
name: string
value: string
}
export async function getExistingAuthToken(cookiePrefix: string): Promise<Cookie | undefined> {
const cookies = await getCookies()
return cookies.getAll().find((cookie) => cookie.name.startsWith(cookiePrefix))
}

View File

@@ -0,0 +1,42 @@
import type { Auth } from 'payload'
import { cookies as getCookies } from 'next/headers.js'
import { generatePayloadCookie } from 'payload'
type SetPayloadAuthCookieArgs = {
authConfig: Auth
cookiePrefix: string
token: string
}
export async function setPayloadAuthCookie({
authConfig,
cookiePrefix,
token,
}: SetPayloadAuthCookieArgs): Promise<void> {
const cookies = await getCookies()
const cookieExpiration = authConfig.tokenExpiration
? new Date(Date.now() + authConfig.tokenExpiration)
: undefined
const payloadCookie = generatePayloadCookie({
collectionAuthConfig: authConfig,
cookiePrefix,
expires: cookieExpiration,
returnCookieAsObject: true,
token,
})
if (payloadCookie.value) {
cookies.set(payloadCookie.name, payloadCookie.value, {
domain: authConfig.cookies.domain,
expires: payloadCookie.expires ? new Date(payloadCookie.expires) : undefined,
httpOnly: true,
sameSite: (typeof authConfig.cookies.sameSite === 'string'
? authConfig.cookies.sameSite.toLowerCase()
: 'lax') as 'lax' | 'none' | 'strict',
secure: authConfig.cookies.secure || false,
})
}
}

View File

@@ -32,6 +32,7 @@ export const APIViewClient: React.FC = () => {
const {
config: {
defaultDepth,
localization,
routes: { api: apiRoute },
serverURL,
@@ -62,7 +63,9 @@ export const APIViewClient: React.FC = () => {
const [data, setData] = React.useState<any>(initialData)
const [draft, setDraft] = React.useState<boolean>(searchParams.get('draft') === 'true')
const [locale, setLocale] = React.useState<string>(searchParams?.get('locale') || code)
const [depth, setDepth] = React.useState<string>(searchParams.get('depth') || '1')
const [depth, setDepth] = React.useState<string>(
searchParams.get('depth') || defaultDepth.toString(),
)
const [authenticated, setAuthenticated] = React.useState<boolean>(true)
const [fullscreen, setFullscreen] = React.useState<boolean>(false)

View File

@@ -1,4 +1,5 @@
import type {
BeforeDocumentControlsServerPropsOnly,
DefaultServerFunctionArgs,
DocumentSlots,
PayloadRequest,
@@ -42,6 +43,18 @@ export const renderDocumentSlots: (args: {
// TODO: Add remaining serverProps
}
const BeforeDocumentControls =
collectionConfig?.admin?.components?.edit?.beforeDocumentControls ||
globalConfig?.admin?.components?.elements?.beforeDocumentControls
if (BeforeDocumentControls) {
components.BeforeDocumentControls = RenderServerComponent({
Component: BeforeDocumentControls,
importMap: req.payload.importMap,
serverProps: serverProps satisfies BeforeDocumentControlsServerPropsOnly,
})
}
const CustomPreviewButton =
collectionConfig?.admin?.components?.edit?.PreviewButton ||
globalConfig?.admin?.components?.elements?.PreviewButton

View File

@@ -31,7 +31,6 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
<span>
{breakpoints.find((bp) => bp.name == breakpoint)?.label ?? customOption.label}
</span>
&nbsp;
<ChevronIcon className={`${baseClass}__chevron`} />
</React.Fragment>
}
@@ -82,7 +81,6 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
button={
<React.Fragment>
<span>{zoom * 100}%</span>
&nbsp;
<ChevronIcon className={`${baseClass}__chevron`} />
</React.Fragment>
}

View File

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

View File

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

View File

@@ -553,6 +553,7 @@ export type FieldRow = {
}
export type DocumentSlots = {
BeforeDocumentControls?: React.ReactNode
Description?: React.ReactNode
PreviewButton?: React.ReactNode
PublishButton?: React.ReactNode
@@ -578,6 +579,9 @@ export type { LanguageOptions } from './LanguageOptions.js'
export type { RichTextAdapter, RichTextAdapterProvider, RichTextHooks } from './RichText.js'
export type {
BeforeDocumentControlsClientProps,
BeforeDocumentControlsServerProps,
BeforeDocumentControlsServerPropsOnly,
DocumentSubViewTypes,
DocumentTabClientProps,
/**

View File

@@ -36,12 +36,12 @@ export type DocumentTabServerPropsOnly = {
readonly permissions: SanitizedPermissions
} & ServerProps
export type DocumentTabServerProps = DocumentTabClientProps & DocumentTabServerPropsOnly
export type DocumentTabClientProps = {
path: string
}
export type DocumentTabServerProps = DocumentTabClientProps & DocumentTabServerPropsOnly
export type DocumentTabCondition = (args: {
collectionConfig: SanitizedCollectionConfig
config: SanitizedConfig
@@ -75,3 +75,10 @@ export type DocumentTabConfig = {
export type DocumentTabComponent = PayloadComponent<{
path: string
}>
// BeforeDocumentControls
export type BeforeDocumentControlsClientProps = {}
export type BeforeDocumentControlsServerPropsOnly = {} & ServerProps
export type BeforeDocumentControlsServerProps = BeforeDocumentControlsClientProps &
BeforeDocumentControlsServerPropsOnly

View File

@@ -36,6 +36,7 @@ export function iterateCollections({
addToImportMap(collection.admin?.components?.beforeListTable)
addToImportMap(collection.admin?.components?.Description)
addToImportMap(collection.admin?.components?.edit?.beforeDocumentControls)
addToImportMap(collection.admin?.components?.edit?.PreviewButton)
addToImportMap(collection.admin?.components?.edit?.PublishButton)
addToImportMap(collection.admin?.components?.edit?.SaveButton)

View File

@@ -279,6 +279,10 @@ export type CollectionAdminOptions = {
* Components within the edit view
*/
edit?: {
/**
* Inject custom components before the document controls
*/
beforeDocumentControls?: CustomComponent[]
/**
* Replaces the "Preview" button
*/

View File

@@ -33,9 +33,9 @@ export const validateUseAsTitle = (config: CollectionConfig) => {
}
}
} else {
if (useAsTitleField && fieldIsVirtual(useAsTitleField)) {
if (useAsTitleField && 'virtual' in useAsTitleField && useAsTitleField.virtual === true) {
throw new InvalidConfiguration(
`The field "${config.admin.useAsTitle}" specified in "admin.useAsTitle" in the collection "${config.slug}" is virtual. A virtual field cannot be used as the title.`,
`The field "${config.admin.useAsTitle}" specified in "admin.useAsTitle" in the collection "${config.slug}" is virtual. A virtual field can be used as the title only when linked to a relationship field.`,
)
}
if (!useAsTitleField) {

View File

@@ -28,6 +28,7 @@ import { buildVersionCollectionFields } from '../../versions/buildCollectionFiel
import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
import { getQueryDraftsSort } from '../../versions/drafts/getQueryDraftsSort.js'
import { sanitizeSortQuery } from './utilities/sanitizeSortQuery.js'
import { buildAfterOperation } from './utils.js'
export type Arguments = {
@@ -96,7 +97,7 @@ export const findOperation = async <
req,
select: incomingSelect,
showHiddenFields,
sort,
sort: incomingSort,
where,
} = args
@@ -143,6 +144,11 @@ export const findOperation = async <
let fullWhere = combineQueries(where, accessResult)
const sort = sanitizeSortQuery({
fields: collection.config.flattenedFields,
sort: incomingSort,
})
const sanitizedJoins = await sanitizeJoinQuery({
collectionConfig,
joins,
@@ -170,7 +176,10 @@ export const findOperation = async <
pagination: usePagination,
req,
select: getQueryDraftsSelect({ select }),
sort: getQueryDraftsSort({ collectionConfig, sort }),
sort: getQueryDraftsSort({
collectionConfig,
sort,
}),
where: fullWhere,
})
} else {

View File

@@ -27,6 +27,7 @@ import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey.js'
import { getQueryDraftsSort } from '../../versions/drafts/getQueryDraftsSort.js'
import { sanitizeSortQuery } from './utilities/sanitizeSortQuery.js'
import { updateDocument } from './utilities/update.js'
import { buildAfterOperation } from './utils.js'
@@ -103,7 +104,7 @@ export const updateOperation = async <
req,
select: incomingSelect,
showHiddenFields,
sort,
sort: incomingSort,
where,
} = args
@@ -136,6 +137,11 @@ export const updateOperation = async <
const fullWhere = combineQueries(where, accessResult)
const sort = sanitizeSortQuery({
fields: collection.config.flattenedFields,
sort: incomingSort,
})
let docs
if (collectionConfig.versions?.drafts && shouldSaveDraft) {

View File

@@ -0,0 +1,51 @@
import type { FlattenedField } from '../../../fields/config/types.js'
const sanitizeSort = ({ fields, sort }: { fields: FlattenedField[]; sort: string }): string => {
let sortProperty = sort
let desc = false
if (sort.indexOf('-') === 0) {
desc = true
sortProperty = sortProperty.substring(1)
}
const segments = sortProperty.split('.')
for (const segment of segments) {
const field = fields.find((each) => each.name === segment)
if (!field) {
return sort
}
if ('fields' in field) {
fields = field.flattenedFields
continue
}
if ('virtual' in field && typeof field.virtual === 'string') {
return `${desc ? '-' : ''}${field.virtual}`
}
}
return sort
}
/**
* Sanitizes the sort parameter, for example virtual fields linked to relationships are replaced with the full path.
*/
export const sanitizeSortQuery = ({
fields,
sort,
}: {
fields: FlattenedField[]
sort?: string | string[]
}): string | string[] | undefined => {
if (!sort) {
return undefined
}
if (Array.isArray(sort)) {
return sort.map((sort) => sanitizeSort({ fields, sort }))
}
return sanitizeSort({ fields, sort })
}

View File

@@ -109,7 +109,6 @@ export const addOrderableFieldsAndHook = (
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, originalDoc, req }) => {
for (const orderableFieldName of orderableFieldNames) {
if (!data[orderableFieldName] && !originalDoc?.[orderableFieldName]) {
console.log('do not enter')
const lastDoc = await req.payload.find({
collection: collection.slug,
depth: 0,
@@ -258,7 +257,6 @@ export const addOrderableEndpoint = (config: SanitizedConfig) => {
},
depth: 0,
req,
select: { id: true },
})
}

View File

@@ -28,22 +28,6 @@ type Args = {
}
)
const flattenWhere = (query: Where): WhereField[] => {
const flattenedConstraints: WhereField[] = []
for (const [key, val] of Object.entries(query)) {
if ((key === 'and' || key === 'or') && Array.isArray(val)) {
for (const subVal of val) {
flattenedConstraints.push(...flattenWhere(subVal))
}
} else {
flattenedConstraints.push({ [key]: val })
}
}
return flattenedConstraints
}
export async function validateQueryPaths({
collectionConfig,
errors = [],
@@ -61,17 +45,47 @@ export async function validateQueryPaths({
const fields = versionFields || (globalConfig || collectionConfig).flattenedFields
if (typeof where === 'object') {
const whereFields = flattenWhere(where)
// We need to determine if the whereKey is an AND, OR, or a schema path
const promises = []
for (const constraint of whereFields) {
for (const path in constraint) {
for (const operator in constraint[path]) {
const val = constraint[path][operator]
for (const path in where) {
const constraint = where[path]
if ((path === 'and' || path === 'or') && Array.isArray(constraint)) {
for (const item of constraint) {
if (collectionConfig) {
promises.push(
validateQueryPaths({
collectionConfig,
errors,
overrideAccess,
policies,
req,
versionFields,
where: item,
}),
)
} else {
promises.push(
validateQueryPaths({
errors,
globalConfig,
overrideAccess,
policies,
req,
versionFields,
where: item,
}),
)
}
}
} else if (!Array.isArray(constraint)) {
for (const operator in constraint) {
const val = constraint[operator]
if (validOperatorSet.has(operator as Operator)) {
promises.push(
validateSearchParam({
collectionConfig,
constraint: where as WhereField,
errors,
fields,
globalConfig,

View File

@@ -2,17 +2,19 @@
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
import type { FlattenedField } from '../../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
import type { PayloadRequest } from '../../types/index.js'
import type { PayloadRequest, WhereField } from '../../types/index.js'
import type { EntityPolicies, PathToQuery } from './types.js'
import { fieldAffectsData, fieldIsVirtual } from '../../fields/config/types.js'
import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
import { getFieldByPath } from '../../utilities/getFieldByPath.js'
import isolateObjectProperty from '../../utilities/isolateObjectProperty.js'
import { getLocalizedPaths } from '../getLocalizedPaths.js'
import { validateQueryPaths } from './validateQueryPaths.js'
type Args = {
collectionConfig?: SanitizedCollectionConfig
constraint: WhereField
errors: { path: string }[]
fields: FlattenedField[]
globalConfig?: SanitizedGlobalConfig
@@ -32,6 +34,7 @@ type Args = {
*/
export async function validateSearchParam({
collectionConfig,
constraint,
errors,
fields,
globalConfig,
@@ -100,8 +103,13 @@ export async function validateSearchParam({
return
}
if (fieldIsVirtual(field)) {
errors.push({ path })
if ('virtual' in field && field.virtual) {
if (field.virtual === true) {
errors.push({ path })
} else {
constraint[`${field.virtual}`] = constraint[path]
delete constraint[path]
}
}
if (polymorphicJoin && path === 'relationTo') {

View File

@@ -269,6 +269,7 @@ export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
siblingData: Partial<TSiblingData>,
{
blockData,
operation,
path,
user,
}: {
@@ -276,6 +277,10 @@ export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
blockData: Partial<TData>
/**
* A string relating to which operation the field type is currently executing within.
*/
operation: Operation
/**
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
*/
@@ -509,9 +514,9 @@ export interface FieldBase {
/**
* Pass `true` to disable field in the DB
* for [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges):
* A virtual field cannot be used in `admin.useAsTitle`
* A virtual field can be used in `admin.useAsTitle` only when linked to a relationship.
*/
virtual?: boolean
virtual?: boolean | string
}
export interface FieldBaseClient {
@@ -1143,6 +1148,7 @@ type SharedRelationshipPropertiesClient = FieldBaseClient &
type RelationshipAdmin = {
allowCreate?: boolean
allowEdit?: boolean
appearance?: 'drawer' | 'select'
components?: {
afterInput?: CustomComponent[]
beforeInput?: CustomComponent[]
@@ -1157,7 +1163,7 @@ type RelationshipAdmin = {
} & Admin
type RelationshipAdminClient = AdminClient &
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'appearance' | 'isSortable'>
export type PolymorphicRelationshipField = {
admin?: {
@@ -1949,7 +1955,7 @@ export function fieldShouldBeLocalized({
}
export function fieldIsVirtual(field: Field | Tab): boolean {
return 'virtual' in field && field.virtual
return 'virtual' in field && Boolean(field.virtual)
}
export type HookName =

View File

@@ -2,7 +2,6 @@
import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { RequestContext } from '../../../index.js'
import type {
JsonObject,
PayloadRequest,
@@ -13,6 +12,7 @@ import type {
import type { Block, Field, TabAsField } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { type RequestContext } from '../../../index.js'
import { getBlockSelect } from '../../../utilities/getBlockSelect.js'
import { stripUnselectedFields } from '../../../utilities/stripUnselectedFields.js'
import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../config/types.js'
@@ -20,6 +20,7 @@ import { getDefaultValue } from '../../getDefaultValue.js'
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
import { traverseFields } from './traverseFields.js'
import { virtualFieldPopulationPromise } from './virtualFieldPopulationPromise.js'
type Args = {
/**
@@ -306,6 +307,24 @@ export const promise = async ({
}
}
if ('virtual' in field && typeof field.virtual === 'string') {
populationPromises.push(
virtualFieldPopulationPromise({
name: field.name,
draft,
fallbackLocale,
fields: (collection || global).flattenedFields,
locale,
overrideAccess,
ref: doc,
req,
segments: field.virtual.split('.'),
showHiddenFields,
siblingDoc,
}),
)
}
// Execute access control
let allowDefaultValue = true
if (triggerAccessControl && field.access && field.access.read) {

View File

@@ -0,0 +1,144 @@
import type { PayloadRequest } from '../../../types/index.js'
import type { FlattenedField } from '../../config/types.js'
import { createDataloaderCacheKey } from '../../../collections/dataloader.js'
export const virtualFieldPopulationPromise = async ({
name,
draft,
fallbackLocale,
fields,
locale,
overrideAccess,
ref,
req,
segments,
showHiddenFields,
siblingDoc,
}: {
draft: boolean
fallbackLocale: string
fields: FlattenedField[]
locale: string
name: string
overrideAccess: boolean
ref: any
req: PayloadRequest
segments: string[]
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
}): Promise<void> => {
const currentSegment = segments.shift()
if (!currentSegment) {
return
}
const currentValue = ref[currentSegment]
if (typeof currentValue === 'undefined') {
return
}
// Final step
if (segments.length === 0) {
siblingDoc[name] = currentValue
return
}
const currentField = fields.find((each) => each.name === currentSegment)
if (!currentField) {
return
}
if (currentField.type === 'group' || currentField.type === 'tab') {
if (!currentValue || typeof currentValue !== 'object') {
return
}
return virtualFieldPopulationPromise({
name,
draft,
fallbackLocale,
fields: currentField.flattenedFields,
locale,
overrideAccess,
ref: currentValue,
req,
segments,
showHiddenFields,
siblingDoc,
})
}
if (
(currentField.type === 'relationship' || currentField.type === 'upload') &&
typeof currentField.relationTo === 'string' &&
!currentField.hasMany
) {
let docID: number | string
if (typeof currentValue === 'object' && currentValue) {
docID = currentValue.id
} else {
docID = currentValue
}
if (typeof docID !== 'string' && typeof docID !== 'number') {
return
}
const select = {}
let currentSelectRef: any = select
const currentFields = req.payload.collections[currentField.relationTo].config.flattenedFields
for (let i = 0; i < segments.length; i++) {
const field = currentFields.find((each) => each.name === segments[i])
const shouldBreak =
i === segments.length - 1 || field?.type === 'relationship' || field?.type === 'upload'
currentSelectRef[segments[i]] = shouldBreak ? true : {}
currentSelectRef = currentSelectRef[segments[i]]
if (shouldBreak) {
break
}
}
const populatedDoc = await req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: currentField.relationTo,
currentDepth: 0,
depth: 0,
docID,
draft,
fallbackLocale,
locale,
overrideAccess,
select,
showHiddenFields,
transactionID: req.transactionID as number,
}),
)
if (!populatedDoc) {
return
}
return virtualFieldPopulationPromise({
name,
draft,
fallbackLocale,
fields: req.payload.collections[currentField.relationTo].config.flattenedFields,
locale,
overrideAccess,
ref: populatedDoc,
req,
segments,
showHiddenFields,
siblingDoc,
})
}
}

View File

@@ -109,7 +109,12 @@ export const promise = async ({
const passesCondition = field.admin?.condition
? Boolean(
field.admin.condition(data, siblingData, { blockData, path: pathSegments, user: req.user }),
field.admin.condition(data, siblingData, {
blockData,
operation,
path: pathSegments,
user: req.user,
}),
)
: true
let skipValidationFromHere = skipValidation || !passesCondition

View File

@@ -9,6 +9,7 @@ import type {
} from '../../admin/types.js'
import type {
Access,
CustomComponent,
EditConfig,
Endpoint,
EntityDescription,
@@ -80,6 +81,10 @@ export type GlobalAdminOptions = {
*/
components?: {
elements?: {
/**
* Inject custom components before the document controls
*/
beforeDocumentControls?: CustomComponent[]
Description?: EntityDescriptionComponent
/**
* Replaces the "Preview" button

View File

@@ -71,7 +71,17 @@ export const getAccess = (config: Config): Record<Operation, Access> =>
return {
and: [
...(typeof constraintAccess === 'object' ? [constraintAccess] : []),
...(typeof constraintAccess === 'object'
? [constraintAccess]
: constraintAccess === false
? [
{
id: {
equals: null,
},
},
]
: []),
{
[`access.${operation}.constraint`]: {
equals: constraint.value,

View File

@@ -78,7 +78,7 @@ export const getConstraints = (config: Config): Field => ({
},
...(config?.queryPresets?.constraints?.[operation]?.reduce(
(acc: Field[], option: QueryPresetConstraint) => {
option.fields.forEach((field, index) => {
option.fields?.forEach((field, index) => {
acc.push({ ...field })
if (fieldAffectsData(field)) {

View File

@@ -25,7 +25,7 @@ export type QueryPreset = {
export type QueryPresetConstraint = {
access: Access<QueryPreset>
fields: Field[]
fields?: Field[]
label: string
value: string
}

View File

@@ -222,8 +222,12 @@ export const handleEndpoints = async ({
}
const response = await handler(req)
return new Response(response.body, {
headers: mergeHeaders(req.responseHeaders ?? new Headers(), response.headers),
headers: headersWithCors({
headers: mergeHeaders(req.responseHeaders ?? new Headers(), response.headers),
req,
}),
status: response.status,
statusText: response.statusText,
})

View File

@@ -6,6 +6,13 @@ export type Autosave = {
* @default 800
*/
interval?: number
/**
* When set to `true`, the "Save as draft" button will be displayed even while autosave is enabled.
* By default, this button is hidden to avoid redundancy with autosave behavior.
*
* @default false
*/
showSaveDraftButton?: boolean
}
export type SchedulePublish = {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.34.0",
"version": "3.35.1",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.34.0",
"version": "3.35.1",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

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

View File

@@ -20,8 +20,7 @@ const baseClass = 'fields-to-export'
export const FieldsToExport: SelectFieldClientComponent = (props) => {
const { id } = useDocumentInfo()
const { path } = props
const { setValue, value } = useField<string[]>({ path })
const { setValue, value } = useField<string[]>()
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
const { getEntityConfig } = useConfig()
const { collection } = useImportExport()

View File

@@ -20,12 +20,12 @@ const baseClass = 'sort-by-fields'
export const SortBy: SelectFieldClientComponent = (props) => {
const { id } = useDocumentInfo()
const { path } = props
const { setValue, value } = useField<string>({ path })
const { setValue, value } = useField<string>()
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
const { query } = useListQuery()
const { getEntityConfig } = useConfig()
const { collection } = useImportExport()
const [displayedValue, setDisplayedValue] = useState<{
id: string
label: ReactNode

View File

@@ -11,6 +11,7 @@ export const WhereField: React.FC = () => {
const { setValue: setSelectionToUseValue, value: selectionToUseValue } = useField({
path: 'selectionToUse',
})
const { setValue } = useField({ path: 'where' })
const { selectAll, selected } = useSelection()
const { query } = useListQuery()

View File

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

View File

@@ -16,8 +16,8 @@ type Props = {
} & RelationshipFieldClientProps
export const TenantField = (args: Props) => {
const { debug, path, unique } = args
const { setValue, value } = useField<number | string>({ path })
const { debug, unique } = args
const { setValue, value } = useField<number | string>()
const { options, selectedTenantID, setPreventRefreshOnChange, setTenant } = useTenantSelection()
const hasSetValueRef = React.useRef(false)

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.34.0",
"version": "3.35.1",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.34.0",
"version": "3.35.1",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.34.0",
"version": "3.35.1",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,43 +1,28 @@
import type { DeleteFromSearch } from '../../types.js'
export const deleteFromSearch: DeleteFromSearch = async ({
collection,
doc,
pluginConfig,
req: { payload },
req,
}) => {
const searchSlug = pluginConfig?.searchOverrides?.slug || 'search'
try {
const searchDocQuery = await payload.find({
collection: searchSlug,
depth: 0,
limit: 1,
pagination: false,
req,
where: {
doc: {
equals: {
relationTo: collection.slug,
value: doc.id,
export const deleteFromSearch: DeleteFromSearch =
(pluginConfig) =>
async ({ id, collection, req: { payload }, req }) => {
const searchSlug = pluginConfig?.searchOverrides?.slug || 'search'
try {
await payload.delete({
collection: searchSlug,
depth: 0,
req,
where: {
'doc.relationTo': {
equals: collection.slug,
},
'doc.value': {
equals: id,
},
},
},
})
if (searchDocQuery?.docs?.[0]) {
await payload.delete({
id: searchDocQuery?.docs?.[0]?.id,
collection: searchSlug,
req,
})
} catch (err: unknown) {
payload.logger.error({
err,
msg: `Error deleting ${searchSlug} doc.`,
})
}
} catch (err: unknown) {
payload.logger.error({
err,
msg: `Error deleting ${searchSlug} doc.`,
})
}
return doc
}

View File

@@ -1,4 +1,4 @@
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, Config } from 'payload'
import type { CollectionAfterChangeHook, Config } from 'payload'
import type { SanitizedSearchPluginConfig, SearchPluginConfig } from './types.js'
@@ -7,7 +7,6 @@ import { syncWithSearch } from './Search/hooks/syncWithSearch.js'
import { generateSearchCollection } from './Search/index.js'
type CollectionAfterChangeHookArgs = Parameters<CollectionAfterChangeHook>[0]
type CollectionAfterDeleteHookArgs = Parameters<CollectionAfterDeleteHook>[0]
export const searchPlugin =
(incomingPluginConfig: SearchPluginConfig) =>
@@ -67,14 +66,9 @@ export const searchPlugin =
})
},
],
afterDelete: [
...(existingHooks?.afterDelete || []),
async (args: CollectionAfterDeleteHookArgs) => {
await deleteFromSearch({
...args,
pluginConfig,
})
},
beforeDelete: [
...(existingHooks?.beforeDelete || []),
deleteFromSearch(pluginConfig),
],
},
}

View File

@@ -1,6 +1,6 @@
import type {
CollectionAfterChangeHook,
CollectionAfterDeleteHook,
CollectionBeforeDeleteHook,
CollectionConfig,
Field,
Locale,
@@ -96,8 +96,4 @@ export type SyncDocArgs = {
// Convert the `collection` arg from `SanitizedCollectionConfig` to a string
export type SyncWithSearch = (Args: SyncWithSearchArgs) => ReturnType<CollectionAfterChangeHook>
export type DeleteFromSearch = (
Args: {
pluginConfig: SearchPluginConfig
} & Parameters<CollectionAfterDeleteHook>[0],
) => ReturnType<CollectionAfterDeleteHook>
export type DeleteFromSearch = (args: SearchPluginConfig) => CollectionBeforeDeleteHook

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
"version": "3.34.0",
"version": "3.35.1",
"description": "Sentry plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.34.0",
"version": "3.35.1",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
'use client'
import type { FieldType, Options } from '@payloadcms/ui'
import type { FieldType } from '@payloadcms/ui'
import type { TextareaFieldClientProps } from 'payload'
import {
@@ -38,7 +38,6 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
required,
},
hasGenerateDescriptionFn,
path,
readOnly,
} = props
@@ -58,12 +57,14 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
const maxLength = maxLengthFromProps || maxLengthDefault
const minLength = minLengthFromProps || minLengthDefault
const { customComponents, errorMessage, setValue, showError, value }: FieldType<string> =
useField({
path,
} as Options)
const { AfterInput, BeforeInput, Label } = customComponents ?? {}
const {
customComponents: { AfterInput, BeforeInput, Label } = {},
errorMessage,
path,
setValue,
showError,
value,
}: FieldType<string> = useField()
const regenerateDescription = useCallback(async () => {
if (!hasGenerateDescriptionFn) {

View File

@@ -1,6 +1,6 @@
'use client'
import type { FieldType, Options } from '@payloadcms/ui'
import type { FieldType } from '@payloadcms/ui'
import type { UploadFieldClientProps } from 'payload'
import {
@@ -30,9 +30,8 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
const {
field: { label, localized, relationTo, required },
hasGenerateImageFn,
path,
readOnly,
} = props || {}
} = props
const {
config: {
@@ -42,10 +41,14 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
getEntityConfig,
} = useConfig()
const field: FieldType<string> = useField({ ...props, path } as Options)
const { customComponents } = field
const { Error, Label } = customComponents ?? {}
const {
customComponents: { Error, Label } = {},
filterOptions,
path,
setValue,
showError,
value,
}: FieldType<string> = useField()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
@@ -53,8 +56,6 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
const { getData } = useForm()
const docInfo = useDocumentInfo()
const { setValue, showError, value } = field
const regenerateImage = useCallback(async () => {
if (!hasGenerateImageFn) {
return
@@ -174,7 +175,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
api={api}
collection={collection}
Error={Error}
filterOptions={field.filterOptions}
filterOptions={filterOptions}
onChange={(incomingImage) => {
if (incomingImage !== null) {
if (typeof incomingImage === 'object') {

View File

@@ -1,6 +1,6 @@
'use client'
import type { FieldType, Options } from '@payloadcms/ui'
import type { FieldType } from '@payloadcms/ui'
import type { TextFieldClientProps } from 'payload'
import {
@@ -33,9 +33,8 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
const {
field: { label, maxLength: maxLengthFromProps, minLength: minLengthFromProps, required },
hasGenerateTitleFn,
path,
readOnly,
} = props || {}
} = props
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
@@ -46,8 +45,14 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
},
} = useConfig()
const field: FieldType<string> = useField({ path } as Options)
const { customComponents: { AfterInput, BeforeInput, Label } = {} } = field
const {
customComponents: { AfterInput, BeforeInput, Label } = {},
errorMessage,
path,
setValue,
showError,
value,
}: FieldType<string> = useField()
const locale = useLocale()
const { getData } = useForm()
@@ -56,8 +61,6 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
const minLength = minLengthFromProps || minLengthDefault
const maxLength = maxLengthFromProps || maxLengthDefault
const { errorMessage, setValue, showError, value } = field
const regenerateTitle = useCallback(async () => {
if (!hasGenerateTitleFn) {
return

View File

@@ -25,4 +25,4 @@ export const es: GenericTranslationsObject = {
tooLong: 'Demasiado largo',
tooShort: 'Demasiado corto',
},
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.34.0",
"version": "3.35.1",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.34.0",
"version": "3.35.1",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -68,12 +68,7 @@ const toolbarGroups = ({ disabledNodes }: IndentFeatureProps): ToolbarGroup[] =>
if (!nodes?.length) {
return false
}
if (nodes.some((node) => disabledNodes?.includes(node.getType()))) {
return false
}
return !$pointsAncestorMatch(selection, (node) =>
(disabledNodes ?? []).includes(node.getType()),
)
return !nodes.some((node) => disabledNodes?.includes(node.getType()))
},
key: 'indentIncrease',
label: ({ i18n }) => {

View File

@@ -36,7 +36,6 @@ const RichTextComponent: React.FC<
editorConfig,
field,
field: {
name,
admin: { className, description, readOnly: readOnlyFromAdmin } = {},
label,
localized,
@@ -48,7 +47,6 @@ const RichTextComponent: React.FC<
} = props
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
const path = pathFromProps ?? name
const editDepth = useEditDepth()
@@ -70,11 +68,12 @@ const RichTextComponent: React.FC<
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled: disabledFromField,
initialValue,
path,
setValue,
showError,
value,
} = useField<SerializedEditorState>({
path,
potentiallyStalePath: pathFromProps,
validate: memoizedValidate,
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.34.0",
"version": "3.35.1",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -28,7 +28,6 @@ import type { LoadedSlateFieldProps } from './types.js'
import { defaultRichTextValue } from '../data/defaultValue.js'
import { richTextValidate } from '../data/validation.js'
import { listTypes } from './elements/listTypes.js'
import './index.scss'
import { hotkeys } from './hotkeys.js'
import { toggleLeaf } from './leaves/toggle.js'
import { withEnterBreakOut } from './plugins/withEnterBreakOut.js'
@@ -37,6 +36,7 @@ import { ElementButtonProvider } from './providers/ElementButtonProvider.js'
import { ElementProvider } from './providers/ElementProvider.js'
import { LeafButtonProvider } from './providers/LeafButtonProvider.js'
import { LeafProvider } from './providers/LeafProvider.js'
import './index.scss'
const baseClass = 'rich-text'
@@ -66,7 +66,6 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
validate = richTextValidate,
} = props
const path = pathFromProps ?? name
const schemaPath = schemaPathFromProps ?? name
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
@@ -97,11 +96,12 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
customComponents: { Description, Error, Label } = {},
disabled: disabledFromField,
initialValue,
path,
setValue,
showError,
value,
} = useField({
path,
potentiallyStalePath: pathFromProps,
validate: memoizedValidate,
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.34.0",
"version": "3.35.1",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
"version": "3.34.0",
"version": "3.35.1",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
"version": "3.34.0",
"version": "3.35.1",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
"version": "3.34.0",
"version": "3.35.1",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
"version": "3.34.0",
"version": "3.35.1",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -197,7 +197,7 @@ export const caTranslations: DefaultTranslationsObject = {
clearAll: 'Esborra-ho tot',
close: 'Tanca',
collapse: 'Replegar',
collections: 'Collections',
collections: 'Col·leccions',
columns: 'Columnes',
columnToSort: 'Columna per ordenar',
confirm: 'Confirma',
@@ -205,7 +205,7 @@ export const caTranslations: DefaultTranslationsObject = {
confirmDeletion: "Confirma l'eliminació",
confirmDuplication: 'Confirma duplicacat',
confirmReindex: 'Reindexa {{collections}}?',
confirmReindexAll: 'Reindexa totes les collections?',
confirmReindexAll: 'Reindexa totes les col·leccions?',
confirmReindexDescription:
'Aixo eliminarà els índexs existents i reindexarà els documents de les col·leccions {{collections}}.',
confirmReindexDescriptionAll:

View File

@@ -54,7 +54,6 @@ export const acceptedLanguages = [
* 'bn-BD',
* 'bn-IN',
* 'bs',
* 'ca',
* 'ca-ES-valencia',
* 'cy',
* 'el',

View File

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

View File

@@ -33,10 +33,6 @@
width: calc(var(--base) * 1.2);
height: calc(var(--base) * 1.2);
svg {
max-width: 1rem;
}
&:hover {
background-color: var(--theme-elevation-200);
}

View File

@@ -6,7 +6,7 @@
}
.btn--withPopup {
margin-block: 24px;
margin-block: 4px;
.btn {
margin: 0;
}

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