Compare commits

..

26 Commits

Author SHA1 Message Date
Alessio Gravili
01dd38a8a7 fix 2025-09-05 15:32:16 -07:00
Alessio Gravili
7a61e9d26e Revert "use dir instead"
This reverts commit deee3f1b34.
2025-09-05 15:15:57 -07:00
Alessio Gravili
deee3f1b34 use dir instead 2025-09-05 14:38:19 -07:00
Alessio Gravili
60cdf50200 feat(richtext-lexical): add basic rtl support 2025-09-05 14:29:42 -07:00
Jarrod Flesch
f288cf6a8f feat(ui): adds admin.autoRefresh root config property (#13682)
Adds the `admin.autoRefresh` property to the root config. This allows
users to stay logged and have their token always refresh in the
background without being prompted with the "Stay Logged In?" modal.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211114366468735
2025-09-05 17:05:18 -04:00
Alessio Gravili
03f7102433 fix: ensure client-side live preview correctly updates for globals (#13718)
Previously, we were sending incorrect API requests for globals.
Additionally, the global findOne endpoint did not respect the data
attribute.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211249618316704
2025-09-05 16:50:42 -04:00
Jarrod Flesch
09e3174834 fix(ui): consistent searchbar across folders and lists (#13712)
Search bar styling was inconsistent across folder and list views. This
re-uses the SearchBar component in the ListHeader component.
2025-09-05 16:40:11 -04:00
Anders Semb Hermansen
a4a0298435 fix: set X-Payload-HTTP-Method-Override as allowed cross origin header (#13717)
### What?

Set  X-Payload-HTTP-Method-Override as allowed cross origin header

### Why?

As of #13619 the live preview uses POST method to retrieve updated data.
When running cms and website on different origins, the cross origin
requires X-Payload-HTTP-Method-Override header to be allowed

### How?

Adding X-Payload-HTTP-Method-Override as a default allowed header
2025-09-05 12:59:37 -07:00
Alessio Gravili
7a8bcdf7ba feat(richtext-lexical): upgrade lexical from 0.34.0 to 0.35.0 (#13715)
This release upgrades the lexical dependency from 0.34.0 to 0.35.0.

If you installed lexical manually, update it to 0.35.0. Installing
lexical manually is not recommended, as it may break between updates,
and our re-exported versions should be used. See the [yellow banner
box](https://payloadcms.com/docs/rich-text/custom-features) for details.

If you still encounter richtext-lexical errors, do the following, in
this order:

- Delete node_modules
- Delete your lockfile (e.g. pnpm-lock.json)
- Reinstall your dependencies (e.g. pnpm install)

### Lexical Breaking Changes

The following Lexical releases describe breaking changes. We recommend
reading them if you're using Lexical APIs directly
(`@payloadcms/richtext-lexical/lexical/*`).

- [v.0.35.0](https://github.com/facebook/lexical/releases/tag/v0.35.0)
2025-09-05 18:48:36 +00:00
Alessio Gravili
6e203db33c feat(live-preview): client-side live preview: simplify population, support hooks and lexical block population (#13619)
Alternative solution to
https://github.com/payloadcms/payload/pull/11104. Big thanks to
@andershermansen and @GermanJablo for kickstarting work on a solution
and bringing this to our attention. This PR copies over the live-preview
test suite example from his PR.

Fixes https://github.com/payloadcms/payload/issues/5285,
https://github.com/payloadcms/payload/issues/6071 and
https://github.com/payloadcms/payload/issues/8277. Potentially fixes
#11801

This PR completely gets rid of our client-side live preview field
traversal + population and all logic related to it, and instead lets the
findByID endpoint handle it.

The data sent through the live preview message event is now passed to
findByID via the newly added `data` attribute. The findByID endpoint
will then use this data and run hooks on it (which run population),
instead of fetching the data from the database.

This new API basically behaves like a `/api/populate?data=` endpoint,
with the benefit that it runs all the hooks. Another use-case for it
will be rendering lexical data. Sometimes you may only have unpopulated
data available. This functionality allows you to then populate the
lexical portion of it on-the-fly, so that you can properly render it to
JSX while displaying images.

## Benefits
- a lot less code to maintain. No duplicative population logic
- much faster - one single API request instead of one request per
relationship to populate
- all payload features are now correctly supported (population and
hooks)
- since hooks are now running for client-side live preview, this means
the `lexicalHTML` field is now supported! This was a long-running issue
- this fixes a lot of population inconsistencies that we previously did
not know of. For example, it previously populated lexical and slate
relationships even if the data was saved in an incorrect format

## [Method Override
(POST)](https://payloadcms.com/docs/rest-api/overview#using-method-override-post)
change

The population request to the findByID endpoint is sent as a post
request, so that we can pass through the `data` without having to
squeeze it into the url params. To do that, it uses the
`X-Payload-HTTP-Method-Override` header.

Previously, this functionality still expected the data to be sent
through as URL search params - just passed to the body instead of the
URL. In this PR, I made it possible to pass it as JSON instead. This
means:

- the receiving endpoint will receive the data under `req.data` and is
not able to read it from the search params
- this means existing endpoints won't support this functionality unless
they also attempt to read from req.data.
- for the purpose of this PR, the findByID endpoint was modified to
support this behavior. This functionality is documented as it can be
useful for user-defined endpoints as well.

Passing data as json has the following benefits:

- it's more performant - no need to serialize and deserialize data to
search params via `qs-esm`. This is especially important here, as we are
passing large amounts of json data
- the current implementation was serializing the data incorrectly,
leading to incorrect data within nested lexical nodes

**Note for people passing their own live preview `requestHandler`:**
instead of sending a GET request to populate documents, you will now
need to send a POST request to the findByID endpoint and pass additional
headers. Additionally, you will need to send through the arguments as
JSON instead of search params and include `data` as an argument. Here is
the updated defaultRequestHandler for reference:

```ts
const defaultRequestHandler: CollectionPopulationRequestHandler = ({
  apiPath,
  data,
  endpoint,
  serverURL,
}) => {
  const url = `${serverURL}${apiPath}/${endpoint}`

  return fetch(url, {
    body: JSON.stringify(data),
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-Payload-HTTP-Method-Override': 'GET',
    },
    method: 'POST',
  })
}
```

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211124793355068
  - https://app.asana.com/0/0/1211124793355066
2025-09-05 14:40:52 -04:00
German Jablonski
0c44c3bdd9 fix(richtext-lexical): add internationalization support for default style label in textStateFeature (#13662)
Fixes #13470

Output: 

<img width="344" height="176" alt="image"
src="https://github.com/user-attachments/assets/6a966e19-0a03-4252-9afa-b8149a95565b"
/>
2025-09-05 17:51:33 +01:00
Yazal Ulloa
008a52d8ec fix(templates): URI encode the cacheTag in getMediaUrl utility - Website template (#13558)
### What?
Encode the cacheTag in the ImageMedia component

### Why?

In the website template, media is render using the updatedAt field as a
cacheTag, this value causes an InvalidQueryStringException when
deploying to Cloudfront

### How?
Uses encodeURIComponent on encode the date value of updatedAt


Fixes https://github.com/payloadcms/payload/issues/13557
2025-09-04 17:45:38 +00:00
Patrik
7e98fbf78e fix(ui): cannot filter by virtual relationship fields in WhereBuilder (#13686)
### What?
- Fixed an issue where virtual relationship fields (`virtual: string`,
e.g. `post.title`) could not be used in the WhereBuilder filter
dropdown.
- Ensured regular virtual fields (`virtual: true`) are still excluded
since they are not backed by database fields.

### Why?
Previously, attempting to filter by a virtual relationship caused
runtime errors because the field was treated as a plain text field. At
the same time, non-queryable virtuals needed to remain excluded to avoid
invalid queries.

### How?
- Updated `reduceFieldsToOptions` to recognize `virtual: string` fields
and map them to their underlying path while keeping their declared type
and operators.
- Continued to filter out `virtual: true` fields in the same guard used
for hidden/disabled fields.
2025-09-04 08:17:10 -07:00
Elliot DeNolf
d109b44856 chore: add AGENTS.md (#13688)
Adds an [`AGENTS.md`](https://agents.md) file.
2025-09-04 16:11:17 +01:00
Patrik
5146fc865f fix(ui): undefined permissions passed in create-first-user view (#13671)
### What?

In the create-first-user view, fields like `richText` were being marked
as `readOnly: true` because they had no permissions entry in the
permissions map.

### Why?

The view was passing an incomplete `docPermissions` object. 

When a field had no entry in `docPermissions.fields`, `renderField`
received `permissions: undefined`, which was interpreted as denied
access.

This caused fields (notably `richText`) to default to read-only even
though the user should have full access when creating the first user.

### How?

- Updated the create-first-user view to always pass a complete
`docPermissions` object.
- Default all fields in the user collection to `{ create: true, read:
true, update: true }`.
- Ensures every field is explicitly granted full access during the
first-user flow.
- Keeps the `renderField` logic unchanged and aligned with Payload’s
permission model.

Fixes #13612 

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211211792037939
2025-09-03 14:16:49 -07:00
Jarrod Flesch
d9e183242c fix: update user session on reset (#13667)
Fixes https://github.com/payloadcms/payload/issues/13637

Adds session logic to password reset so the user is logged in after
resetting the password, similar to how it works when useSessions is
false.
2025-09-03 17:00:02 -04:00
Aapo Laakkio
7794541af3 fix(ui): don't populate on auto save (#13649)
Edited by @jacobsfletch

Autosave is run with default depth, causing form state and the document
info context to be inconsistent across the load/autosave/save lifecycle.

For example, when loading the app, the document is queried with `depth:
0` and relationships are _not_ populated. Same with save/draft/publish.
Autosave, on the other hand, populates these relationships, causing the
data to temporarily change in shape in between these events.

Here's an example:


https://github.com/user-attachments/assets/153ea112-7591-4f54-9216-575322f4edbe

Now, autosave runs with `depth: 0` as expected, consistent with the
other events of the document lifecycle.

**Plus this is a huge performance improvement.**

Fixes #13643 and #13192.

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

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-09-03 16:59:36 -04:00
German Jablonski
1a1696d9ae feat(richtext-lexical): upgrade lexical from 0.28.0 to 0.34.0 (#13622)
Fixes #13386

Below I write a clarification to copy and paste into the release note,
based on our latest upgrade of Lexical [in
v3.29.0](https://github.com/payloadcms/payload/releases/tag/v3.29.0).

## Important
This release upgrades the lexical dependency from 0.28.0 to 0.34.0.

If you installed lexical manually, update it to 0.34.0. Installing
lexical manually is not recommended, as it may break between updates,
and our re-exported versions should be used. See the [yellow banner
box](https://payloadcms.com/docs/rich-text/custom-features) for details.

If you still encounter richtext-lexical errors, do the following, in
this order:

- Delete node_modules
- Delete your lockfile (e.g. pnpm-lock.json)
- Reinstall your dependencies (e.g. pnpm install)

### Lexical Breaking Changes

The following Lexical releases describe breaking changes. We recommend
reading them if you're using Lexical APIs directly
(`@payloadcms/richtext-lexical/lexical/*`).

- [v.0.33.0](https://github.com/facebook/lexical/releases/tag/v0.33.0)
- [v.0.30.0](https://github.com/facebook/lexical/releases/tag/v0.30.0)
- [v.0.29.0](https://github.com/facebook/lexical/releases/tag/v0.29.0)

___

TODO:
- [x] https://github.com/facebook/lexical/pull/7719
- [x] https://github.com/facebook/lexical/pull/7362
- [x] https://github.com/facebook/lexical/pull/7707
- [x] https://github.com/facebook/lexical/pull/7388
- [x] https://github.com/facebook/lexical/pull/7357
- [x] https://github.com/facebook/lexical/pull/7352
- [x] https://github.com/facebook/lexical/pull/7472
- [x] https://github.com/facebook/lexical/pull/7556
- [x] https://github.com/facebook/lexical/pull/7417
- [x] https://github.com/facebook/lexical/pull/1036
- [x] https://github.com/facebook/lexical/pull/7509
- [x] https://github.com/facebook/lexical/pull/7693
- [x] https://github.com/facebook/lexical/pull/7408
- [x] https://github.com/facebook/lexical/pull/7450
- [x] https://github.com/facebook/lexical/pull/7415
- [x] https://github.com/facebook/lexical/pull/7368
- [x] https://github.com/facebook/lexical/pull/7372
- [x] https://github.com/facebook/lexical/pull/7572
- [x] https://github.com/facebook/lexical/pull/7558
- [x] https://github.com/facebook/lexical/pull/7613
- [x] https://github.com/facebook/lexical/pull/7405
- [x] https://github.com/facebook/lexical/pull/7420
- [x] https://github.com/facebook/lexical/pull/7662

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211202581885926
2025-09-03 18:49:10 +00:00
Jacob Fletcher
b8d7ccb4dc fix(ui): use consistent row ids when duplicating array and block rows (#13679)
Fixes #13653.

Duplicating array rows causes phantom rows to appear. This is because
when duplicate the row locally, we use inconsistent row IDs, e.g. the
`array.rows[0].id` does not match its `array.0.id` counterpart. This
causes form state to lose the reference to the existing row, which the
server interprets as new row as of #13551.

Before:


https://github.com/user-attachments/assets/9f7efc59-ebd9-4fbb-b643-c22d4d3140a3

After:


https://github.com/user-attachments/assets/188db823-4ee5-4757-8b89-751c8d978ad9

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211210023936585
2025-09-03 14:29:39 -04:00
Patrik
be47f65b7c fix: prevent enabling trash on folders (#13675)
### What?

This PR updates the folder configuration types to explicitly omit the
`trash` option from `CollectionConfig` when used for folders.

### Why?

Currently, only documents are designed to support trashing. Allowing
folders to be trashed introduces inconsistencies, since removing a
folder does not properly remove its references from associated
documents.

By omitting `trash` from folder configurations, we prevent accidental
use of unsupported behavior until proper design and handling for folder
trashing is implemented.

### How?

- Updated `RootFoldersConfiguration.collectionOverrides` type to use
`Omit<CollectionConfig, 'trash'>`
- Ensures `trash` cannot be enabled on folders
2025-09-03 11:04:48 -07:00
Jessica Rynkar
0f6d748365 feat: adds new experimental.localizeStatus option (#13207)
### What?
Adds a new `experimental.localizeStatus` config option, set to `false`
by default. When `true`, the admin panel will display the document
status based on the *current locale* instead of the _latest_ overall
status. Also updates the edit view to only show a `changed` status when
`autosave` is enabled.

### Why?
Showing the status for the current locale is more accurate and useful in
multi-locale setups. This update will become default behavior, able to
be opted in by setting `experimental.localizeStatus: true` in the
Payload config. This option will become depreciated in V4.

### How?
When `localizeStatus` is `true`, we store the localized status in a new
`localeStatus` field group within version data. The admin panel then
reads from this field to display the correct status for the current
locale.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-09-03 10:27:08 +01:00
Alessio Gravili
a11586811e fix(ui): field.admin.condition data attribute missing document ID when document is being edited (#13676)
Fixes https://github.com/payloadcms/payload/issues/10379

During form state requests, the passed `data` did not have access to the
document ID. This was because the data we use came from the client,
passed as an argument. The client did not pass data that included the
document ID.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211203844178567
2025-09-03 01:14:07 +00:00
German Jablonski
9b109339ee fix(ui): await for publish success to update the UI (#13673)
Fixes #13664

Before:


https://github.com/user-attachments/assets/083577f5-9006-4b35-9799-3df704ed84a8

After:


https://github.com/user-attachments/assets/70a89a5b-bed4-447c-94f6-57bf3dbd2a70



---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211210023936582
2025-09-02 21:10:25 +01:00
German Jablonski
4e972c3fe2 docs(plugin-form-builder): add RadioField documentation (#13529)
### What?
Adds comprehensive documentation for the RadioField component in the
form builder plugin documentation.

### Why?
Addresses review feedback from #11908 noting that the RadioField was not
documented after being exported for end-user use.

### How?
- Added RadioField section with complete property documentation
- Added detailed Radio Options sub-section explaining option structure
- Updated Select field documentation for consistency (added missing
placeholder property and detailed options structure)
- Added radio field to configuration example to show all available
fields

Fixes the documentation gap identified in #11908 review comments.
2025-09-02 13:38:23 -04:00
dependabot[bot]
cfb70f06bb ci(deps): bump the github_actions group across 3 directories with 4 updates (#13654)
Bumps the github_actions group with 4 updates in the / directory:
[actions/checkout](https://github.com/actions/checkout),
[slackapi/slack-github-action](https://github.com/slackapi/slack-github-action),
[SethCohen/github-releases-to-discord](https://github.com/sethcohen/github-releases-to-discord)
and
[amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request).
Bumps the github_actions group with 1 update in the
/.github/actions/triage directory:
[actions/checkout](https://github.com/actions/checkout).
Bumps the github_actions group with 4 updates in the /.github/workflows
directory: [actions/checkout](https://github.com/actions/checkout),
[slackapi/slack-github-action](https://github.com/slackapi/slack-github-action),
[SethCohen/github-releases-to-discord](https://github.com/sethcohen/github-releases-to-discord)
and
[amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request).

Updates `actions/checkout` from 4 to 5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
<h2>v4.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
<li>Prepare release v4.3.0 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2237">actions/checkout#2237</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/motss"><code>@​motss</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li><a href="https://github.com/mouismail"><code>@​mouismail</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li><a href="https://github.com/benwells"><code>@​benwells</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v4.3.0">https://github.com/actions/checkout/compare/v4...v4.3.0</a></p>
<h2>v4.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.1...v4.2.2">https://github.com/actions/checkout/compare/v4.2.1...v4.2.2</a></p>
<h2>v4.2.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1919">actions/checkout#1919</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.0...v4.2.1">https://github.com/actions/checkout/compare/v4.2.0...v4.2.1</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08c6903cd8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li><a
href="9f265659d3"><code>9f26565</code></a>
Update actions checkout to use node 24 (<a
href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />

Updates `slackapi/slack-github-action` from 2.1.0 to 2.1.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/slackapi/slack-github-action/releases">slackapi/slack-github-action's
releases</a>.</em></p>
<blockquote>
<h2>Slack Send v2.1.1</h2>
<h2>What's Changed</h2>
<p>This release fixes an issue where substituted variables might've
broken valid JSON or YAML parsings when using the
<code>payload-file-path</code> input option.</p>
<h3>🐛 Bug fixes</h3>
<ul>
<li>fix: parse provided payloads before replacing templated variables in
<a
href="https://redirect.github.com/slackapi/slack-github-action/pull/449">slackapi/slack-github-action#449</a>
- Thanks <a
href="https://github.com/zimeg"><code>@​zimeg</code></a>!</li>
</ul>
<h3>📚 Documentation</h3>
<ul>
<li>docs: fix channel mention formatting in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/447">slackapi/slack-github-action#447</a>
- Thanks <a
href="https://github.com/mwbrooks"><code>@​mwbrooks</code></a>!</li>
<li>docs: remove links to pages that are no longer referenced in
markdown in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/459">slackapi/slack-github-action#459</a>
- Thanks <a
href="https://github.com/zimeg"><code>@​zimeg</code></a>!</li>
</ul>
<h3>🤖 Dependencies</h3>
<ul>
<li>build(deps): bump undici from 5.28.5 to 5.29.0 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/442">slackapi/slack-github-action#442</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps): bump codecov/codecov-action from 5.4.2 to 5.4.3 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/443">slackapi/slack-github-action#443</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump mocha from 11.1.0 to 11.5.0 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/450">slackapi/slack-github-action#450</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps): bump <code>@​actions/github</code> from 6.0.0 to 6.0.1
in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/451">slackapi/slack-github-action#451</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump <code>@​types/node</code> from 22.15.3 to
22.15.29 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/452">slackapi/slack-github-action#452</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps): bump <code>@​slack/web-api</code> from 7.9.1 to 7.9.2
in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/453">slackapi/slack-github-action#453</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps): bump <code>@​slack/web-api</code> from 7.9.2 to 7.9.3
in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/462">slackapi/slack-github-action#462</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps): bump axios from 1.9.0 to 1.10.0 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/465">slackapi/slack-github-action#465</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump <code>@​types/node</code> from 22.15.29 to
24.0.3 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/466">slackapi/slack-github-action#466</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump mocha from 11.5.0 to 11.7.1 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/468">slackapi/slack-github-action#468</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump mocha-suppress-logs from 0.5.1 to 0.6.0 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/469">slackapi/slack-github-action#469</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump sinon from 20.0.0 to 21.0.0 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/471">slackapi/slack-github-action#471</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump <code>@​types/node</code> from 24.0.3 to
24.0.8 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/472">slackapi/slack-github-action#472</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
<li>build(deps-dev): bump <code>@​biomejs/biome</code> from 1.9.4 to
2.0.6 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/470">slackapi/slack-github-action#470</a>
- Thanks <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>!</li>
</ul>
<h3>🧰 Maintenance</h3>
<ul>
<li>ci: pin action hashes and escape variables with minimum permission
in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/441">slackapi/slack-github-action#441</a>
- Thanks <a
href="https://github.com/zimeg"><code>@​zimeg</code></a>!</li>
<li>build: create separate release branches for tagged releases on
publish in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/457">slackapi/slack-github-action#457</a>
- Thanks <a
href="https://github.com/zimeg"><code>@​zimeg</code></a>!</li>
<li>build: clone repository &quot;docs&quot; and configuration when
syncing project docs in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/467">slackapi/slack-github-action#467</a>
- Thanks <a
href="https://github.com/lukegalbraithrussell"><code>@​lukegalbraithrussell</code></a>!</li>
<li>chore(release): tag version 2.1.1 in <a
href="https://redirect.github.com/slackapi/slack-github-action/pull/474">slackapi/slack-github-action#474</a>
- Thanks <a
href="https://github.com/zimeg"><code>@​zimeg</code></a>!</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/slackapi/slack-github-action/compare/v2.1.0...v2.1.1">https://github.com/slackapi/slack-github-action/compare/v2.1.0...v2.1.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="91efab103c"><code>91efab1</code></a>
Release</li>
<li><a
href="b6f4640825"><code>b6f4640</code></a>
chore(release): tag version 2.1.1 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/474">#474</a>)</li>
<li><a
href="d3dc61e5d1"><code>d3dc61e</code></a>
build(deps-dev): bump <code>@​biomejs/biome</code> from 1.9.4 to 2.0.6
(<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/470">#470</a>)</li>
<li><a
href="f647c89261"><code>f647c89</code></a>
build(deps-dev): bump <code>@​types/node</code> from 24.0.3 to 24.0.8
(<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/472">#472</a>)</li>
<li><a
href="e6fa63302e"><code>e6fa633</code></a>
build(deps-dev): bump sinon from 20.0.0 to 21.0.0 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/471">#471</a>)</li>
<li><a
href="75b7822f87"><code>75b7822</code></a>
build(deps-dev): bump mocha-suppress-logs from 0.5.1 to 0.6.0 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/469">#469</a>)</li>
<li><a
href="d7b6150e2a"><code>d7b6150</code></a>
build(deps-dev): bump mocha from 11.5.0 to 11.7.1 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/468">#468</a>)</li>
<li><a
href="a7f5b68f29"><code>a7f5b68</code></a>
build: clone repository &quot;docs&quot; and configuration when syncing
project docs (#...</li>
<li><a
href="c69deab257"><code>c69deab</code></a>
build(deps-dev): bump <code>@​types/node</code> from 22.15.29 to 24.0.3
(<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/466">#466</a>)</li>
<li><a
href="1d0943cb8c"><code>1d0943c</code></a>
build(deps): bump axios from 1.9.0 to 1.10.0 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/465">#465</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/slackapi/slack-github-action/compare/v2.1.0...v2.1.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `SethCohen/github-releases-to-discord` from 1.16.2 to 1.19.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sethcohen/github-releases-to-discord/releases">SethCohen/github-releases-to-discord's
releases</a>.</em></p>
<blockquote>
<h2>v1.19.0</h2>
<h2><a
href="https://github.com/SethCohen/github-releases-to-discord/compare/v1.18.0...v1.19.0">1.19.0</a>
(2025-06-17)</h2>
<h3>Features</h3>
<ul>
<li><strong>tests:</strong> add Jest configuration and comprehensive
tests for utility functions in index.js to ensure functionality and
reliability (<a
href="0559b87ee8">0559b87</a>)</li>
</ul>
<h3>Miscellaneous</h3>
<ul>
<li>added updated dependencies (<a
href="067d2cb017">067d2cb</a>)</li>
<li><strong>package:</strong> update <code>@​actions/github</code>
dependency to version 6.0.1 and add Jest as a dev dependency with a test
script (<a
href="0559b87ee8">0559b87</a>)</li>
</ul>
<h2>v1.18.0</h2>
<h2><a
href="https://github.com/SethCohen/github-releases-to-discord/compare/v1.17.0...v1.18.0">1.18.0</a>
(2025-06-17)</h2>
<h3>Features</h3>
<ul>
<li><strong>index.js:</strong> enhance sendWebhook function to handle
rate limits with retries for improved reliability when sending requests
to Discord (<a
href="feb5a40237">feb5a40</a>)</li>
</ul>
<h3>Miscellaneous</h3>
<ul>
<li>remove unnecessary test file from .gitignore and add sample test
release JSON for local testing (<a
href="82d906cc6f">82d906c</a>)</li>
<li>update README for clarity and conciseness, improve formatting, and
add new sections for better user guidance (<a
href="82d906cc6f">82d906c</a>)</li>
</ul>
<h2>v1.17.0</h2>
<h2><a
href="https://github.com/SethCohen/github-releases-to-discord/compare/v1.16.2...v1.17.0">1.17.0</a>
(2025-06-17)</h2>
<h3>Features</h3>
<ul>
<li><strong>workflow:</strong> add GitHub Actions workflow to
automatically update SemVer tags on tag push events (<a
href="e768ce1023">e768ce1</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/SethCohen/github-releases-to-discord/blob/master/CHANGELOG.md">SethCohen/github-releases-to-discord's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/SethCohen/github-releases-to-discord/compare/v1.18.0...v1.19.0">1.19.0</a>
(2025-06-17)</h2>
<h3>Features</h3>
<ul>
<li><strong>tests:</strong> add Jest configuration and comprehensive
tests for utility functions in index.js to ensure functionality and
reliability (<a
href="0559b87ee8">0559b87</a>)</li>
</ul>
<h3>Miscellaneous</h3>
<ul>
<li>added updated dependencies (<a
href="067d2cb017">067d2cb</a>)</li>
<li><strong>package:</strong> update <code>@​actions/github</code>
dependency to version 6.0.1 and add Jest as a dev dependency with a test
script (<a
href="0559b87ee8">0559b87</a>)</li>
</ul>
<h2><a
href="https://github.com/SethCohen/github-releases-to-discord/compare/v1.17.0...v1.18.0">1.18.0</a>
(2025-06-17)</h2>
<h3>Features</h3>
<ul>
<li><strong>index.js:</strong> enhance sendWebhook function to handle
rate limits with retries for improved reliability when sending requests
to Discord (<a
href="feb5a40237">feb5a40</a>)</li>
</ul>
<h3>Miscellaneous</h3>
<ul>
<li>remove unnecessary test file from .gitignore and add sample test
release JSON for local testing (<a
href="82d906cc6f">82d906c</a>)</li>
<li>update README for clarity and conciseness, improve formatting, and
add new sections for better user guidance (<a
href="82d906cc6f">82d906c</a>)</li>
</ul>
<h2><a
href="https://github.com/SethCohen/github-releases-to-discord/compare/v1.16.2...v1.17.0">1.17.0</a>
(2025-06-17)</h2>
<h3>Features</h3>
<ul>
<li><strong>workflow:</strong> add GitHub Actions workflow to
automatically update SemVer tags on tag push events (<a
href="e768ce1023">e768ce1</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b96a33520f"><code>b96a335</code></a>
chore(master): release 1.19.0 (<a
href="https://redirect.github.com/sethcohen/github-releases-to-discord/issues/50">#50</a>)</li>
<li><a
href="067d2cb017"><code>067d2cb</code></a>
chore: added updated dependencies</li>
<li><a
href="0559b87ee8"><code>0559b87</code></a>
feat(tests): add Jest configuration and comprehensive tests for utility
funct...</li>
<li><a
href="de60879a86"><code>de60879</code></a>
chore(master): release 1.18.0 (<a
href="https://redirect.github.com/sethcohen/github-releases-to-discord/issues/49">#49</a>)</li>
<li><a
href="82d906cc6f"><code>82d906c</code></a>
chore: update README for clarity and conciseness, improve formatting,
and add...</li>
<li><a
href="feb5a40237"><code>feb5a40</code></a>
feat(index.js): enhance sendWebhook function to handle rate limits with
retri...</li>
<li><a
href="9fe781fdc7"><code>9fe781f</code></a>
Add custom url (<a
href="https://redirect.github.com/sethcohen/github-releases-to-discord/issues/44">#44</a>)</li>
<li><a
href="74ded4247d"><code>74ded42</code></a>
Add option to strip PR and commit links (<a
href="https://redirect.github.com/sethcohen/github-releases-to-discord/issues/48">#48</a>)</li>
<li><a
href="e1dc0826fe"><code>e1dc082</code></a>
chore(master): release 1.17.0 (<a
href="https://redirect.github.com/sethcohen/github-releases-to-discord/issues/47">#47</a>)</li>
<li><a
href="e768ce1023"><code>e768ce1</code></a>
feat(workflow): add GitHub Actions workflow to automatically update
SemVer ta...</li>
<li>See full diff in <a
href="https://github.com/sethcohen/github-releases-to-discord/compare/v1.16.2...v1.19.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `amannn/action-semantic-pull-request` from 5 to 6
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/amannn/action-semantic-pull-request/releases">amannn/action-semantic-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.3...v6.0.0">6.0.0</a>
(2025-08-13)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li>Upgrade action to use Node.js 24 and ESM (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/287">#287</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Upgrade action to use Node.js 24 and ESM (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/287">#287</a>)
(<a
href="bc0c9a79ab">bc0c9a7</a>)</li>
</ul>
<h2>v5.5.3</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.2...v5.5.3">5.5.3</a>
(2024-06-28)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Bump <code>braces</code> dependency (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/269">#269</a>.
by <a href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a>)
(<a
href="2d952a1bf9">2d952a1</a>)</li>
</ul>
<h2>v5.5.2</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.1...v5.5.2">5.5.2</a>
(2024-04-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Bump tar from 6.1.11 to 6.2.1 (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/262">#262</a>
by <a href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a>)
(<a
href="9a90d5a5ac">9a90d5a</a>)</li>
</ul>
<h2>v5.5.1</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.0...v5.5.1">5.5.1</a>
(2024-04-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Bump ip from 2.0.0 to 2.0.1 (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/263">#263</a>
by <a href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a>)
(<a
href="5e7e9acca3">5e7e9ac</a>)</li>
</ul>
<h2>v5.5.0</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.4.0...v5.5.0">5.5.0</a>
(2024-04-23)</h2>
<h3>Features</h3>
<ul>
<li>Add outputs for <code>type</code>, <code>scope</code> and
<code>subject</code> (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/261">#261</a>
by <a href="https://github.com/bcaurel"><code>@​bcaurel</code></a>) (<a
href="b05f5f6423">b05f5f6</a>)</li>
</ul>
<h2>v5.4.0</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.3.0...v5.4.0">5.4.0</a>
(2023-11-03)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md">amannn/action-semantic-pull-request's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.2.0...v5.3.0">5.3.0</a>
(2023-09-25)</h2>
<h3>Features</h3>
<ul>
<li>Use Node.js 20 in action (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/240">#240</a>)
(<a
href="4c0d5a21fc">4c0d5a2</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.1.0...v5.2.0">5.2.0</a>
(2023-03-16)</h2>
<h3>Features</h3>
<ul>
<li>Update dependencies by <a
href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a> (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/229">#229</a>)
(<a
href="e797448a07">e797448</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.0.2...v5.1.0">5.1.0</a>
(2023-02-10)</h2>
<h3>Features</h3>
<ul>
<li>Add regex support to <code>scope</code> and
<code>disallowScopes</code> configuration (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/226">#226</a>)
(<a
href="403a6f8924">403a6f8</a>)</li>
</ul>
<h3><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.0.1...v5.0.2">5.0.2</a>
(2022-10-17)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>Upgrade <code>@actions/core</code> to avoid deprecation warnings (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/208">#208</a>)
(<a
href="91f4126c9e">91f4126</a>)</li>
</ul>
<h3><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.0.0...v5.0.1">5.0.1</a>
(2022-10-14)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>Upgrade GitHub Action to use Node v16 (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/207">#207</a>)
(<a
href="6282ee339b">6282ee3</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v4.6.0...v5.0.0">5.0.0</a>
(2022-10-11)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li>Enum options need to be newline delimited (to allow whitespace
within them) (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/205">#205</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Enum options need to be newline delimited (to allow whitespace
within them) (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/205">#205</a>)
(<a
href="c906fe1e5a">c906fe1</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v4.5.0...v4.6.0">4.6.0</a>
(2022-09-26)</h2>
<h3>Features</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="48f256284b"><code>48f2562</code></a>
chore: Release 6.1.1 [skip ci]</li>
<li><a
href="800da4c97f"><code>800da4c</code></a>
fix: Parse <code>headerPatternCorrespondence</code> properly (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/295">#295</a>)</li>
<li><a
href="677b89571e"><code>677b895</code></a>
test: Fix broken test</li>
<li><a
href="24e6f016c1"><code>24e6f01</code></a>
ci: Fix permissions for tagger</li>
<li><a
href="7f33ba7922"><code>7f33ba7</code></a>
chore: Release 6.1.0 [skip ci]</li>
<li><a
href="afa4edb1c4"><code>afa4edb</code></a>
fix: Remove trailing whitespace from &quot;unknown release type&quot;
error message (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/291">#291</a>)</li>
<li><a
href="a30288bf13"><code>a30288b</code></a>
feat: Support providing regexps for types (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/292">#292</a>)</li>
<li><a
href="a46a7c8dc4"><code>a46a7c8</code></a>
build: Move Vitest to <code>devDependencies</code> (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/290">#290</a>)</li>
<li><a
href="fdd4d3ddf6"><code>fdd4d3d</code></a>
chore: Release 6.0.1 [skip ci]</li>
<li><a
href="58e4ab40f5"><code>58e4ab4</code></a>
fix: Actually execute action (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/289">#289</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />

Updates `actions/checkout` from 4 to 5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
<h2>v4.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
<li>Prepare release v4.3.0 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2237">actions/checkout#2237</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/motss"><code>@​motss</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li><a href="https://github.com/mouismail"><code>@​mouismail</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li><a href="https://github.com/benwells"><code>@​benwells</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v4.3.0">https://github.com/actions/checkout/compare/v4...v4.3.0</a></p>
<h2>v4.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.1...v4.2.2">https://github.com/actions/checkout/compare/v4.2.1...v4.2.2</a></p>
<h2>v4.2.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1919">actions/checkout#1919</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.0...v4.2.1">https://github.com/actions/checkout/compare/v4.2.0...v4.2.1</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08c6903cd8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li><a
href="9f265659d3"><code>9f26565</code></a>
Update actions checkout to use node 24 (<a
href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />

Updates `actions/checkout` from 4 to 5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
<h2>v4.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
<li>Prepare release v4.3.0 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2237">actions/checkout#2237</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/motss"><code>@​motss</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li><a href="https://github.com/mouismail"><code>@​mouismail</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li><a href="https://github.com/benwells"><code>@​benwells</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v4.3.0">https://github.com/actions/checkout/compare/v4...v4.3.0</a></p>
<h2>v4.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.1...v4.2.2">https://github.com/actions/checkout/compare/v4.2.1...v4.2.2</a></p>
<h2>v4.2.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1919">actions/checkout#1919</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.0...v4.2.1">https://github.com/actions/checkout/compare/v4.2.0...v4.2.1</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08c6903cd8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li><a
href="9f265659d3"><code>9f26565</code></a>
Update actions checkout to use node 24 (<a
href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />

Updates `slackapi/slack-github-action` from 2.1.0 to 2.1.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/slackapi/slack-github-action/releases">slackapi/slack-github-action's
releases</a>.</em></p>
<blockquote>
<h2>Slack Send v2.1.1</h2>
<h2>What's Changed</h2>
<p>This release fixes an issue...

_Description has been truncated_

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-02 10:00:09 -04:00
Jarrod Flesch
1c68ed5251 fix(ui): sidebar missing sticky top offset (#13652)
Regression from https://github.com/payloadcms/payload/pull/13340.
Incorrect fallback value for the `--sidebar-wrap-top` css variable was
used.

### Before
<img width="2896" height="1200" alt="CleanShot 2025-09-01 at 10 03
39@2x"
src="https://github.com/user-attachments/assets/caf054d2-ac61-4a96-b6a1-3981ce9f528f"
/>

### After
<img width="2894" height="1194" alt="CleanShot 2025-09-01 at 10 02
59@2x"
src="https://github.com/user-attachments/assets/1e13e084-6f62-47bb-88b3-921ba5f3ff10"
/>
2025-09-02 13:51:07 +00:00
154 changed files with 2298 additions and 2221 deletions

View File

@@ -26,7 +26,7 @@ runs:
steps:
- name: Checkout code
if: ${{ github.event_name != 'pull_request' }}
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Run action
run: node ${{ github.action_path }}/dist/index.js
shell: sh

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup
uses: ./.github/actions/setup
@@ -34,7 +34,7 @@ jobs:
- name: Slack notification on failure
if: failure()
uses: slackapi/slack-github-action@v2.1.0
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ inputs.debug == 'true' && secrets.SLACK_TEST_WEBHOOK_URL || secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook

View File

@@ -11,7 +11,7 @@ jobs:
name: Repository dispatch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Dispatch event
if: ${{ github.event_name == 'workflow_dispatch' }}

View File

@@ -33,7 +33,7 @@ jobs:
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -62,7 +62,7 @@ jobs:
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -78,7 +78,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -98,7 +98,7 @@ jobs:
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -122,7 +122,7 @@ jobs:
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -184,7 +184,7 @@ jobs:
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -309,7 +309,7 @@ jobs:
env:
SUITE_NAME: ${{ matrix.suite }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -447,7 +447,7 @@ jobs:
env:
SUITE_NAME: ${{ matrix.suite }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -550,7 +550,7 @@ jobs:
MONGODB_VERSION: 6.0
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -647,7 +647,7 @@ jobs:
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup
@@ -706,7 +706,7 @@ jobs:
actions: read # for fetching base branch bundle stats
pull-requests: write # for comments
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Node setup
uses: ./.github/actions/setup

View File

@@ -17,7 +17,7 @@ jobs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
sparse-checkout: .github/workflows
@@ -54,7 +54,7 @@ jobs:
POSTGRES_DB: payloadtests
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup
uses: ./.github/actions/setup

View File

@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: ./.github/actions/release-commenter
continue-on-error: true
env:
@@ -43,9 +43,9 @@ jobs:
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Github Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.16.2
uses: SethCohen/github-releases-to-discord@v1.19.0
with:
webhook_url: ${{ secrets.DISCORD_RELEASES_WEBHOOK_URL }}
color: '16777215'

View File

@@ -14,7 +14,7 @@ jobs:
name: lint-pr-title
runs-on: ubuntu-24.04
steps:
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@v6
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup
uses: ./.github/actions/setup
- name: Load npm token

View File

@@ -90,7 +90,7 @@ jobs:
if: github.event_name == 'issues'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.base.ref }}
token: ${{ secrets.GITHUB_TOKEN }}

54
AGENTS.md Normal file
View File

@@ -0,0 +1,54 @@
# Payload Monorepo Agent Instructions
## Project Structure
- Packages are located in the `packages/` directory.
- The main Payload package is `packages/payload`. This contains the core functionality.
- Database adapters are in `packages/db-*`.
- The UI package is `packages/ui`.
- The Next.js integration is in `packages/next`.
- Rich text editor packages are in `packages/richtext-*`.
- Storage adapters are in `packages/storage-*`.
- Email adapters are in `packages/email-*`.
- Plugins which add additional functionality are in `packages/plugin-*`.
- Documentation is in the `docs/` directory.
- Monorepo tooling is in the `tools/` directory.
- Test suites and configs are in the `test/` directory.
- LLMS.txt is at URL: https://payloadcms.com/llms.txt
- LLMS-FULL.txt is at URL: https://payloadcms.com/llms-full.txt
## Dev environment tips
- Any package can be built using a `pnpm build:*` script defined in the root `package.json`. These typically follow the format `pnpm build:<directory_name>`. The options are all of the top-level directories inside the `packages/` directory. Ex `pnpm build:db-mongodb` which builds the `packages/db-mongodb` package.
- ALL packages can be built with `pnpm build:all`.
- Use `pnpm dev` to start the monorepo dev server. This loads the default config located at `test/_community/config.ts`.
- Specific dev configs for each package can be run with `pnpm dev <directory_name>`. The options are all of the top-level directories inside the `test/` directory. Ex `pnpm dev fields` which loads the `test/fields/config.ts` config. The directory name can either encompass a single area of functionality or be the name of a specific package.
## Testing instructions
- There are unit, integration, and e2e tests in the monorepo.
- Unit tests can be run with `pnpm test:unit`.
- Integration tests can be run with `pnpm test:int`. Individual test suites can be run with `pnpm test:int <directory_name>`, which will point at `test/<directory_name>/int.spec.ts`.
- E2E tests can be run with `pnpm test:e2e`.
- All tests can be run with `pnpm test`.
- Prefer running `pnpm test:int` for verifying local code changes.
## PR Guidelines
- This repository follows conventional commits for PR titles
- PR Title format: <type>(<scope>): <title>. Title must start with a lowercase letter.
- Valid types are build, chore, ci, docs, examples, feat, fix, perf, refactor, revert, style, templates, test
- Prefer `feat` for new features and `fix` for bug fixes.
- Valid scopes are the following regex patterns: cpa, db-\*, db-mongodb, db-postgres, db-vercel-postgres, db-sqlite, drizzle, email-\*, email-nodemailer, email-resend, eslint, graphql, live-preview, live-preview-react, next, payload-cloud, plugin-cloud, plugin-cloud-storage, plugin-form-builder, plugin-import-export, plugin-multi-tenant, plugin-nested-docs, plugin-redirects, plugin-search, plugin-sentry, plugin-seo, plugin-stripe, richtext-\*, richtext-lexical, richtext-slate, storage-\*, storage-azure, storage-gcs, storage-uploadthing, storage-vercel-blob, storage-s3, translations, ui, templates, examples(\/(\w|-)+)?, deps
- Scopes should be chosen based upon the package(s) being modified. If multiple packages are being modified, choose the most relevant one or no scope at all.
- Example PR titles:
- `feat(db-mongodb): add support for transactions`
- `feat(richtext-lexical): add options to hide block handles`
- `fix(ui): json field type ignoring editorOptions`
## Commit Guidelines
- This repository follows conventional commits for commit messages
- The first commit of a branch should follow the PR title format: <type>(<scope>): <title>. Follow the same rules as PR titles.
- Subsequent commits should prefer `chore` commits without a scope unless a specific package is being modified.
- These will eventually be squashed into the first commit when merging the PR.

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -98,6 +98,7 @@ The following options are available:
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
| `autoRefresh` | Used to automatically refresh user tokens for users logged into the dashboard. [More details](../authentication/overview). |
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |

View File

@@ -173,6 +173,25 @@ The following options are available:
| **`password`** | The password of the user to login as. This is only needed if `prefillOnly` is set to true |
| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. |
## Auto-Refresh
Turning this property on will allow users to stay logged in indefinitely while their browser is open and on the admin panel, by automatically refreshing their authentication token before it expires.
To enable auto-refresh for user tokens, set `autoRefresh: true` in the [Payload Config](../admin/overview#admin-options) to:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
// highlight-start
admin: {
autoRefresh: true,
},
// highlight-end
})
```
## Operations
All auth-related operations are available via Payload's REST, Local, and GraphQL APIs. These operations are automatically added to your Collection when you enable Authentication. [More details](./operations).

View File

@@ -131,6 +131,29 @@ localization: {
Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.
## Experimental Options
Experimental options are features that may not be fully stable and may change or be removed in future releases.
These options can be enabled in your Payload Config under the `experimental` key. You can set them like this:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
experimental: {
localizeStatus: true,
},
})
```
The following experimental options are available related to localization:
| Option | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
## Field Localization
Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.

View File

@@ -70,6 +70,7 @@ The following options are available:
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
| **`experimental`** | Configure experimental features for Payload. These may be unstable and may change or be removed in future releases. [More details](../experimental). |
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |

View File

@@ -0,0 +1,66 @@
---
title: Experimental Features
label: Overview
order: 10
desc: Enable and configure experimental functionality within Payload. These featuresmay be unstable and may change or be removed without notice.
keywords: experimental, unstable, beta, preview, features, configuration, Payload, cms, headless, javascript, node, react, nextjs
---
Experimental features allow you to try out new functionality before it becomes a stable part of Payload. These features may still be in active development, may have incomplete functionality, and can change or be removed in future releases without warning.
## How It Works
Experimental features are configured via the root-level `experimental` property in your [Payload Config](../configuration/overview). This property contains individual feature flags, each flag can be configured independently, allowing you to selectively opt into specific functionality.
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
experimental: {
localizeStatus: true, // highlight-line
},
})
```
## Experimental Options
The following options are available:
| Option | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
This list may change without notice.
## When to Use Experimental Features
You might enable an experimental feature when:
- You want early access to new capabilities before their stable release.
- You can accept the risks of using potentially unstable functionality.
- You are testing new features in a development or staging environment.
- You wish to provide feedback to the Payload team on new functionality.
If you are working on a production application, carefully evaluate whether the benefits outweigh the risks. For most stable applications, it is recommended to wait until the feature is officially released.
<Banner type="success">
<strong>Tip:</strong> To stay up to date on experimental features or share
your feedback, visit the{' '}
<a
href="https://github.com/payloadcms/payload/discussions"
target="_blank"
rel="noopener noreferrer"
>
Payload GitHub Discussions
</a>{' '}
or{' '}
<a
href="https://github.com/payloadcms/payload/issues"
target="_blank"
rel="noopener noreferrer"
>
open an issue
</a>
.
</Banner>

View File

@@ -79,6 +79,7 @@ formBuilderPlugin({
text: true,
textarea: true,
select: true,
radio: true,
email: true,
state: true,
country: true,
@@ -293,14 +294,46 @@ Maps to a `textarea` input on your front-end. Used to collect a multi-line strin
Maps to a `select` input on your front-end. Used to display a list of options.
| Property | Type | Description |
| -------------- | -------- | -------------------------------------------------------- |
| `name` | string | The name of the field. |
| `label` | string | The label of the field. |
| `defaultValue` | string | The default value of the field. |
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
| `options` | array | An array of objects with `label` and `value` properties. |
| Property | Type | Description |
| -------------- | -------- | ------------------------------------------------------------------------------- |
| `name` | string | The name of the field. |
| `label` | string | The label of the field. |
| `defaultValue` | string | The default value of the field. |
| `placeholder` | string | The placeholder text for the field. |
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
| `options` | array | An array of objects that define the select options. See below for more details. |
#### Select Options
Each option in the `options` array defines a selectable choice for the select field.
| Property | Type | Description |
| -------- | ------ | ----------------------------------- |
| `label` | string | The display text for the option. |
| `value` | string | The value submitted for the option. |
### Radio
Maps to radio button inputs on your front-end. Used to allow users to select a single option from a list of choices.
| Property | Type | Description |
| -------------- | -------- | ------------------------------------------------------------------------------ |
| `name` | string | The name of the field. |
| `label` | string | The label of the field. |
| `defaultValue` | string | The default value of the field. |
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
| `options` | array | An array of objects that define the radio options. See below for more details. |
#### Radio Options
Each option in the `options` array defines a selectable choice for the radio field.
| Property | Type | Description |
| -------- | ------ | ----------------------------------- |
| `label` | string | The display text for the option. |
| `value` | string | The value submitted for the option. |
### Email (field)

View File

@@ -773,3 +773,28 @@ const res = await fetch(`${api}/${collectionSlug}?depth=1&locale=en`, {
},
})
```
### Passing as JSON
When using `X-Payload-HTTP-Method-Override`, it expects the body to be a query string. If you want to pass JSON instead, you can set the `Content-Type` to `application/json` and include the JSON body in the request.
#### Example
```ts
const res = await fetch(`${api}/${collectionSlug}/${id}`, {
// Only the findByID endpoint supports HTTP method overrides with JSON data
method: 'POST',
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
'X-Payload-HTTP-Method-Override': 'GET',
},
body: JSON.stringify({
depth: 1,
locale: 'en',
}),
})
```
This can be more efficient for large JSON payloads, as you avoid converting data to and from query strings. However, only certain endpoints support this. Supported endpoints will read the parsed body under a `data` property, instead of reading from query parameters as with standard GET requests.

View File

@@ -11,7 +11,7 @@ keywords: lexical, richtext, html
There are two main approaches to convert your Lexical-based rich text to HTML:
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API.
### On-demand
@@ -101,10 +101,7 @@ export const MyRSCComponent = async ({
### HTML field
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended for two reasons:
1. It creates a column with duplicate content in another format.
2. In [client-side live preview](/docs/live-preview/client), it makes it not "live".
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended, as it creates a column with duplicate content in another format.
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.

1
next-env.d.ts vendored
View File

@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -12,6 +12,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
autosave,
createdAt,
globalSlug,
localeStatus,
parent,
publishedLocale,
req,
@@ -33,6 +34,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
autosave,
createdAt,
latest: true,
localeStatus,
parent,
publishedLocale,
snapshot,

View File

@@ -12,6 +12,7 @@ export const createVersion: CreateVersion = async function createVersion(
autosave,
collectionSlug,
createdAt,
localeStatus,
parent,
publishedLocale,
req,
@@ -37,6 +38,7 @@ export const createVersion: CreateVersion = async function createVersion(
autosave,
createdAt,
latest: true,
localeStatus,
parent,
publishedLocale,
snapshot,

View File

@@ -179,6 +179,13 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
for (let i = 0; i < result.docs.length; i++) {
const id = result.docs[i].parent
const localeStatus = result.docs[i].localeStatus || {}
if (locale && localeStatus[locale]) {
result.docs[i].status = localeStatus[locale]
result.docs[i].version._status = localeStatus[locale]
}
result.docs[i] = result.docs[i].version ?? {}
result.docs[i].id = id
}

View File

@@ -15,6 +15,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
autosave,
createdAt,
globalSlug,
localeStatus,
publishedLocale,
req,
returning,
@@ -35,6 +36,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
autosave,
createdAt,
latest: true,
localeStatus,
publishedLocale,
snapshot,
updatedAt,

View File

@@ -15,6 +15,7 @@ export async function createVersion<T extends TypeWithID>(
autosave,
collectionSlug,
createdAt,
localeStatus,
parent,
publishedLocale,
req,
@@ -40,6 +41,7 @@ export async function createVersion<T extends TypeWithID>(
autosave,
createdAt,
latest: true,
localeStatus,
parent,
publishedLocale,
snapshot,

View File

@@ -36,15 +36,17 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
where: combinedWhere,
})
return {
...result,
docs: result.docs.map((doc) => {
doc = {
id: doc.parent,
...doc.version,
}
for (let i = 0; i < result.docs.length; i++) {
const id = result.docs[i].parent
const localeStatus = result.docs[i].localeStatus || {}
if (locale && localeStatus[locale]) {
result.docs[i].status = localeStatus[locale]
result.docs[i].version._status = localeStatus[locale]
}
return doc
}),
result.docs[i] = result.docs[i].version ?? {}
result.docs[i].id = id
}
return result
}

View File

@@ -1,22 +1,12 @@
import type { FieldSchemaJSON } from 'payload'
import type { CollectionPopulationRequestHandler, LivePreviewMessageEvent } from './types.js'
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
import { mergeData } from './mergeData.js'
const _payloadLivePreview: {
fieldSchema: FieldSchemaJSON | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
previousData: any
} = {
/**
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
* We need to cache this value so that it can be used across subsequent messages
* To do this, save `fieldSchemaJSON` when it arrives as a global variable
* Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
*/
fieldSchema: undefined,
/**
* Each time the data is merged, cache the result as a `previousData` variable
* This will ensure changes compound overtop of each other
@@ -35,26 +25,13 @@ export const handleMessage = async <T extends Record<string, any>>(args: {
const { apiRoute, depth, event, initialData, requestHandler, serverURL } = args
if (isLivePreviewEvent(event, serverURL)) {
const { data, externallyUpdatedRelationship, fieldSchemaJSON, locale } = event.data
if (!_payloadLivePreview?.fieldSchema && fieldSchemaJSON) {
_payloadLivePreview.fieldSchema = fieldSchemaJSON
}
if (!_payloadLivePreview?.fieldSchema) {
// eslint-disable-next-line no-console
console.warn(
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
)
return initialData
}
const { collectionSlug, data, globalSlug, locale } = event.data
const mergedData = await mergeData<T>({
apiRoute,
collectionSlug,
depth,
externallyUpdatedRelationship,
fieldSchema: _payloadLivePreview.fieldSchema,
globalSlug,
incomingData: data,
initialData: _payloadLivePreview?.previousData || initialData,
locale,

View File

@@ -4,6 +4,5 @@ export { isLivePreviewEvent } from './isLivePreviewEvent.js'
export { mergeData } from './mergeData.js'
export { ready } from './ready.js'
export { subscribe } from './subscribe.js'
export { traverseRichText } from './traverseRichText.js'
export type { LivePreviewMessageEvent } from './types.js'
export { unsubscribe } from './unsubscribe.js'

View File

@@ -1,115 +1,60 @@
import type { DocumentEvent, FieldSchemaJSON, PaginatedDocs } from 'payload'
import type { CollectionPopulationRequestHandler } from './types.js'
import type { CollectionPopulationRequestHandler, PopulationsByCollection } from './types.js'
import { traverseFields } from './traverseFields.js'
const defaultRequestHandler = ({
const defaultRequestHandler: CollectionPopulationRequestHandler = ({
apiPath,
data,
endpoint,
serverURL,
}: {
apiPath: string
endpoint: string
serverURL: string
}) => {
const url = `${serverURL}${apiPath}/${endpoint}`
return fetch(url, {
body: JSON.stringify(data),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Payload-HTTP-Method-Override': 'GET',
},
method: 'POST',
})
}
// Relationships are only updated when their `id` or `relationTo` changes, by comparing the old and new values
// This needs to also happen when locale changes, except this is not not part of the API response
// Instead, we keep track of the old locale ourselves and trigger a re-population when it changes
let prevLocale: string | undefined
export const mergeData = async <T extends Record<string, any>>(args: {
apiRoute?: string
/**
* @deprecated Use `requestHandler` instead
*/
collectionPopulationRequestHandler?: CollectionPopulationRequestHandler
collectionSlug?: string
depth?: number
externallyUpdatedRelationship?: DocumentEvent
fieldSchema: FieldSchemaJSON
globalSlug?: string
incomingData: Partial<T>
initialData: T
locale?: string
requestHandler?: CollectionPopulationRequestHandler
returnNumberOfRequests?: boolean
serverURL: string
}): Promise<
{
_numberOfRequests?: number
} & T
> => {
}): Promise<T> => {
const {
apiRoute,
collectionSlug,
depth,
externallyUpdatedRelationship,
fieldSchema,
globalSlug,
incomingData,
initialData,
locale,
returnNumberOfRequests,
serverURL,
} = args
const result = { ...initialData }
const requestHandler = args.requestHandler || defaultRequestHandler
const populationsByCollection: PopulationsByCollection = {}
const result = await requestHandler({
apiPath: apiRoute || '/api',
data: {
data: incomingData,
depth,
locale,
},
endpoint: encodeURI(
`${globalSlug ? 'globals/' : ''}${collectionSlug ?? globalSlug}${collectionSlug ? `/${initialData.id}` : ''}`,
),
serverURL,
}).then((res) => res.json())
traverseFields({
externallyUpdatedRelationship,
fieldSchema,
incomingData,
localeChanged: prevLocale !== locale,
populationsByCollection,
result,
})
await Promise.all(
Object.entries(populationsByCollection).map(async ([collection, populations]) => {
let res: PaginatedDocs
const ids = new Set(populations.map(({ id }) => id))
const requestHandler =
args.collectionPopulationRequestHandler || args.requestHandler || defaultRequestHandler
try {
res = await requestHandler({
apiPath: apiRoute || '/api',
endpoint: encodeURI(
`${collection}?depth=${depth}&limit=${ids.size}&where[id][in]=${Array.from(ids).join(',')}${locale ? `&locale=${locale}` : ''}`,
),
serverURL,
}).then((res) => res.json())
if (res?.docs?.length > 0) {
res.docs.forEach((doc) => {
populationsByCollection[collection]?.forEach((population) => {
if (population.id === doc.id) {
population.ref[population.accessor] = doc
}
})
})
}
} catch (err) {
console.error(err) // eslint-disable-line no-console
}
}),
)
prevLocale = locale
return {
...result,
...(returnNumberOfRequests
? { _numberOfRequests: Object.keys(populationsByCollection).length }
: {}),
}
return result
}

View File

@@ -1,299 +0,0 @@
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
import type { PopulationsByCollection } from './types.js'
import { traverseRichText } from './traverseRichText.js'
export const traverseFields = <T extends Record<string, any>>(args: {
externallyUpdatedRelationship?: DocumentEvent
fieldSchema: FieldSchemaJSON
incomingData: T
localeChanged: boolean
populationsByCollection: PopulationsByCollection
result: Record<string, any>
}): void => {
const {
externallyUpdatedRelationship,
fieldSchema: fieldSchemas,
incomingData,
localeChanged,
populationsByCollection,
result,
} = args
fieldSchemas.forEach((fieldSchema) => {
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
const fieldName = fieldSchema.name
switch (fieldSchema.type) {
case 'array':
if (
!incomingData[fieldName] &&
incomingData[fieldName] !== undefined &&
result?.[fieldName] !== undefined
) {
result[fieldName] = []
}
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
if (!result[fieldName]) {
result[fieldName] = []
}
if (!result[fieldName][i]) {
result[fieldName][i] = {}
}
traverseFields({
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields!,
incomingData: incomingRow,
localeChanged,
populationsByCollection,
result: result[fieldName][i],
})
return result[fieldName][i]
})
}
break
case 'blocks':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
const incomingBlockJSON = fieldSchema.blocks?.[incomingBlock.blockType]
if (!result[fieldName]) {
result[fieldName] = []
}
if (
!result[fieldName][i] ||
result[fieldName][i].id !== incomingBlock.id ||
result[fieldName][i].blockType !== incomingBlock.blockType
) {
result[fieldName][i] = {
blockType: incomingBlock.blockType,
}
}
traverseFields({
externallyUpdatedRelationship,
fieldSchema: incomingBlockJSON!.fields!,
incomingData: incomingBlock,
localeChanged,
populationsByCollection,
result: result[fieldName][i],
})
return result[fieldName][i]
})
} else {
result[fieldName] = []
}
break
case 'group':
// falls through
case 'tabs':
if (!result[fieldName]) {
result[fieldName] = {}
}
traverseFields({
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields!,
incomingData: incomingData[fieldName] || {},
localeChanged,
populationsByCollection,
result: result[fieldName],
})
break
case 'relationship':
// falls through
case 'upload':
// Handle `hasMany` relationships
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
if (!result[fieldName] || !incomingData[fieldName].length) {
result[fieldName] = []
}
incomingData[fieldName].forEach((incomingRelation, i) => {
// Handle `hasMany` polymorphic
if (Array.isArray(fieldSchema.relationTo)) {
// if the field doesn't exist on the result, create it
// the value will be populated later
if (!result[fieldName][i]) {
result[fieldName][i] = {
relationTo: incomingRelation.relationTo,
}
}
const oldID = result[fieldName][i]?.value?.id
const oldRelation = result[fieldName][i]?.relationTo
const newID = incomingRelation.value
const newRelation = incomingRelation.relationTo
const hasChanged = newID !== oldID || newRelation !== oldRelation
const hasUpdated =
newRelation === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
if (hasChanged || hasUpdated || localeChanged) {
if (!populationsByCollection[newRelation]) {
populationsByCollection[newRelation] = []
}
populationsByCollection[newRelation].push({
id: incomingRelation.value,
accessor: 'value',
ref: result[fieldName][i],
})
}
} else {
// Handle `hasMany` monomorphic
const hasChanged = incomingRelation !== result[fieldName][i]?.id
const hasUpdated =
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
incomingRelation === externallyUpdatedRelationship?.id
if (hasChanged || hasUpdated || localeChanged) {
if (!populationsByCollection[fieldSchema.relationTo!]) {
populationsByCollection[fieldSchema.relationTo!] = []
}
populationsByCollection[fieldSchema.relationTo!]?.push({
id: incomingRelation,
accessor: i,
ref: result[fieldName],
})
}
}
})
} else {
// Handle `hasOne` polymorphic
if (Array.isArray(fieldSchema.relationTo)) {
// if the field doesn't exist on the result, create it
// the value will be populated later
if (!result[fieldName]) {
result[fieldName] = {
relationTo: incomingData[fieldName]?.relationTo,
}
}
const hasNewValue =
incomingData[fieldName] &&
typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName] !== null
const hasOldValue =
result[fieldName] &&
typeof result[fieldName] === 'object' &&
result[fieldName] !== null
const newID = hasNewValue
? typeof incomingData[fieldName].value === 'object'
? incomingData[fieldName].value.id
: incomingData[fieldName].value
: ''
const oldID = hasOldValue
? typeof result[fieldName].value === 'object'
? result[fieldName].value.id
: result[fieldName].value
: ''
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
const hasChanged = newID !== oldID || newRelation !== oldRelation
const hasUpdated =
newRelation === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
// if the new value/relation is different from the old value/relation
// populate the new value, otherwise leave it alone
if (hasChanged || hasUpdated || localeChanged) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
if (!populationsByCollection[newRelation]) {
populationsByCollection[newRelation] = []
}
populationsByCollection[newRelation].push({
id: newID,
accessor: 'value',
ref: result[fieldName],
})
} else {
result[fieldName] = null
}
}
} else {
// Handle `hasOne` monomorphic
const newID: number | string | undefined =
(incomingData[fieldName] &&
typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName].id) ||
incomingData[fieldName]
const oldID: number | string | undefined =
(result[fieldName] &&
typeof result[fieldName] === 'object' &&
result[fieldName].id) ||
result[fieldName]
const hasChanged = newID !== oldID
const hasUpdated =
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
// if the new value is different from the old value
// populate the new value, otherwise leave it alone
if (hasChanged || hasUpdated || localeChanged) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
if (!populationsByCollection[fieldSchema.relationTo!]) {
populationsByCollection[fieldSchema.relationTo!] = []
}
populationsByCollection[fieldSchema.relationTo!]?.push({
id: newID,
accessor: fieldName,
ref: result as Record<string, unknown>,
})
} else {
result[fieldName] = null
}
}
}
}
break
case 'richText':
result[fieldName] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[fieldName],
populationsByCollection,
result: result[fieldName],
})
break
default:
result[fieldName] = incomingData[fieldName]
}
}
})
}

View File

@@ -1,105 +0,0 @@
import type { DocumentEvent } from 'payload'
import type { PopulationsByCollection } from './types.js'
export const traverseRichText = ({
externallyUpdatedRelationship,
incomingData,
populationsByCollection,
result,
}: {
externallyUpdatedRelationship?: DocumentEvent
incomingData: any
populationsByCollection: PopulationsByCollection
result: any
}): any => {
if (Array.isArray(incomingData)) {
if (!result) {
result = []
}
result = incomingData.map((item, index) => {
if (!result[index]) {
result[index] = item
}
return traverseRichText({
externallyUpdatedRelationship,
incomingData: item,
populationsByCollection,
result: result[index],
})
})
} else if (incomingData && typeof incomingData === 'object') {
if (!result) {
result = {}
}
// Remove keys from `result` that do not appear in `incomingData`
// There's likely another way to do this,
// But recursion and references make this very difficult
Object.keys(result).forEach((key) => {
if (!(key in incomingData)) {
delete result[key]
}
})
// Iterate over the keys of `incomingData` and populate `result`
Object.keys(incomingData).forEach((key) => {
if (!result[key]) {
// Instantiate the key in `result` if it doesn't exist
// Ensure its type matches the type of the `incomingData`
// We don't have a schema to check against here
result[key] =
incomingData[key] && typeof incomingData[key] === 'object'
? Array.isArray(incomingData[key])
? []
: {}
: undefined
}
const isRelationship = key === 'value' && 'relationTo' in incomingData
if (isRelationship) {
// or if there are no keys besides id
const needsPopulation =
!result.value ||
typeof result.value !== 'object' ||
(typeof result.value === 'object' &&
Object.keys(result.value).length === 1 &&
'id' in result.value)
const hasChanged =
result &&
typeof result === 'object' &&
result.value.id === externallyUpdatedRelationship?.id
if (needsPopulation || hasChanged) {
if (!populationsByCollection[incomingData.relationTo]) {
populationsByCollection[incomingData.relationTo] = []
}
populationsByCollection[incomingData.relationTo]?.push({
id:
incomingData[key] && typeof incomingData[key] === 'object'
? incomingData[key].id
: incomingData[key],
accessor: 'value',
ref: result,
})
}
} else {
result[key] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[key],
populationsByCollection,
result: result[key],
})
}
})
} else {
result = incomingData
}
return result
}

View File

@@ -1,11 +1,13 @@
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
import type { DocumentEvent } from 'payload'
export type CollectionPopulationRequestHandler = ({
apiPath,
data,
endpoint,
serverURL,
}: {
apiPath: string
data: Record<string, any>
endpoint: string
serverURL: string
}) => Promise<Response>
@@ -14,18 +16,11 @@ export type LivePreviewArgs = {}
export type LivePreview = void
export type PopulationsByCollection = {
[slug: string]: Array<{
accessor: number | string
id: number | string
ref: Record<string, unknown>
}>
}
export type LivePreviewMessageEvent<T> = MessageEvent<{
collectionSlug?: string
data: T
externallyUpdatedRelationship?: DocumentEvent
fieldSchemaJSON: FieldSchemaJSON
globalSlug?: string
locale?: string
type: 'payload-live-preview'
}>

View File

@@ -17,6 +17,11 @@ export async function refresh({ config }: { config: any }) {
throw new Error('Cannot refresh token: user not authenticated')
}
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
if (!existingCookie) {
return { message: 'No valid token found to refresh', success: false }
}
const collection: CollectionSlug | undefined = result.user.collection
const collectionConfig = payload.collections[collection]
@@ -35,15 +40,10 @@ export async function refresh({ config }: { config: any }) {
return { message: 'Token refresh failed', success: false }
}
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
if (!existingCookie) {
return { message: 'No valid token found to refresh', success: false }
}
await setPayloadAuthCookie({
authConfig: collectionConfig.config.auth,
cookiePrefix: payload.config.cookiePrefix,
token: existingCookie.value,
token: refreshResult.refreshedToken,
})
return { message: 'Token refreshed successfully', success: true }

View File

@@ -1,11 +1,14 @@
import type { AdminViewServerProps } from 'payload'
import type {
AdminViewServerProps,
SanitizedDocumentPermissions,
SanitizedFieldsPermissions,
} from 'payload'
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
import React from 'react'
import { getDocPreferences } from '../Document/getDocPreferences.js'
import { getDocumentData } from '../Document/getDocumentData.js'
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
import { CreateFirstUserClient } from './index.client.js'
import './index.scss'
@@ -43,18 +46,27 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
user: req.user,
})
// Get permissions
const { docPermissions } = await getDocumentPermissions({
collectionConfig,
data,
req,
})
const baseFields: SanitizedFieldsPermissions = Object.fromEntries(
collectionConfig.fields
.filter((f): f is { name: string } & typeof f => 'name' in f && typeof f.name === 'string')
.map((f) => [f.name, { create: true, read: true, update: true }]),
)
// In create-first-user we should always allow all fields
const docPermissionsForForm: SanitizedDocumentPermissions = {
create: true,
delete: true,
fields: baseFields,
read: true,
readVersions: true,
update: true,
}
// Build initial form state from data
const { state: formState } = await buildFormState({
collectionSlug: collectionConfig.slug,
data,
docPermissions,
docPermissions: docPermissionsForForm,
docPreferences,
locale: locale?.code,
operation: 'create',
@@ -69,7 +81,7 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
<h1>{req.t('general:welcome')}</h1>
<p>{req.t('authentication:beginCreateFirstUser')}</p>
<CreateFirstUserClient
docPermissions={docPermissions}
docPermissions={docPermissionsForForm}
docPreferences={docPreferences}
initialState={formState}
loginWithUsername={loginWithUsername}

View File

@@ -19,6 +19,7 @@ type AutosaveCellProps = {
rowData: {
autosave?: boolean
id: number | string
localeStatus?: Record<string, 'draft' | 'published'>
publishedLocale?: string
version: {
_status: string

View File

@@ -12,6 +12,7 @@ import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { getFieldsToSign } from '../getFieldsToSign.js'
import { jwtSign } from '../jwt.js'
import { addSessionToUser } from '../sessions.js'
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
import { generatePasswordSaltHash } from '../strategies/local/generatePasswordSaltHash.js'
@@ -143,12 +144,25 @@ export const resetPasswordOperation = async <TSlug extends CollectionSlug>(
await authenticateLocalStrategy({ doc, password: data.password })
const fieldsToSign = getFieldsToSign({
const fieldsToSignArgs: Parameters<typeof getFieldsToSign>[0] = {
collectionConfig,
email: user.email,
user,
}
const { sid } = await addSessionToUser({
collectionConfig,
payload,
req,
user,
})
if (sid) {
fieldsToSignArgs.sid = sid
}
const fieldsToSign = getFieldsToSign(fieldsToSignArgs)
const { token } = await jwtSign({
fieldsToSign,
secret,

View File

@@ -11,16 +11,21 @@ import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
import { findByIDOperation } from '../operations/findByID.js'
export const findByIDHandler: PayloadHandler = async (req) => {
const { searchParams } = req
const { data, searchParams } = req
const { id, collection } = getRequestCollectionWithID(req)
const depth = searchParams.get('depth')
const trash = searchParams.get('trash') === 'true'
const depth = data ? data.depth : searchParams.get('depth')
const trash = data ? data.trash : searchParams.get('trash') === 'true'
const result = await findByIDOperation({
id,
collection,
data: data
? data?.data
: searchParams.get('data')
? JSON.parse(searchParams.get('data') as string)
: undefined,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
draft: data ? data.draft : searchParams.get('draft') === 'true',
joins: sanitizeJoinParams(req.query.joins as JoinParams),
populate: sanitizePopulateParam(req.query.populate),
req,

View File

@@ -291,6 +291,7 @@ export const createOperation = async <
autosave,
collection: collectionConfig,
docWithLocales: result,
locale,
operation: 'create',
payload,
publishSpecificLocale,

View File

@@ -32,6 +32,11 @@ import { buildAfterOperation } from './utils.js'
export type Arguments = {
collection: Collection
currentDepth?: number
/**
* You may pass the document data directly which will skip the `db.findOne` database query.
* This is useful if you want to use this endpoint solely for running hooks and populating data.
*/
data?: Record<string, unknown>
depth?: number
disableErrors?: boolean
draft?: boolean
@@ -163,7 +168,8 @@ export const findByIDOperation = async <
throw new NotFound(t)
}
let result: DataFromCollectionSlug<TSlug> = (await req.payload.db.findOne(findOneArgs))!
let result: DataFromCollectionSlug<TSlug> =
(args.data as DataFromCollectionSlug<TSlug>) ?? (await req.payload.db.findOne(findOneArgs))!
if (!result) {
if (!disableErrors) {

View File

@@ -41,6 +41,11 @@ export type Options<
* @internal
*/
currentDepth?: number
/**
* You may pass the document data directly which will skip the `db.findOne` database query.
* This is useful if you want to use this endpoint solely for running hooks and populating data.
*/
data?: Record<string, unknown>
/**
* [Control auto-population](https://payloadcms.com/docs/queries/depth) of nested relationship and upload fields.
*/
@@ -126,6 +131,7 @@ export async function findByIDLocal<
id,
collection: collectionSlug,
currentDepth,
data,
depth,
disableErrors = false,
draft = false,
@@ -150,6 +156,7 @@ export async function findByIDLocal<
id,
collection,
currentDepth,
data,
depth,
disableErrors,
draft,

View File

@@ -316,6 +316,7 @@ export const updateDocument = async <
collection: collectionConfig,
docWithLocales: result,
draft: shouldSaveDraft,
locale,
operation: 'update',
payload,
publishSpecificLocale,

View File

@@ -159,6 +159,16 @@ export const createClientConfig = ({
break
case 'experimental':
if (config.experimental) {
clientConfig.experimental = {}
if (config.experimental?.localizeStatus) {
clientConfig.experimental.localizeStatus = config.experimental.localizeStatus
}
}
break
case 'folders':
if (config.folders) {
clientConfig.folders = {

View File

@@ -47,6 +47,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
defaultDepth: 2,
defaultMaxTextLength: 40000,
endpoints: [],
experimental: {},
globals: [],
graphQL: {
disablePlaygroundInProduction: true,
@@ -121,6 +122,7 @@ export const addDefaultsToConfig = (config: Config): Config => {
config.defaultDepth = config.defaultDepth ?? 2
config.defaultMaxTextLength = config.defaultMaxTextLength ?? 40000
config.endpoints = config.endpoints ?? []
config.experimental = config.experimental ?? {}
config.globals = config.globals ?? []
config.graphQL = {
disableIntrospectionInProduction: true,

View File

@@ -721,6 +721,14 @@ export type ImportMapGenerators = Array<
}) => void
>
/**
* Experimental features.
* These may be unstable and may change or be removed in future releases.
*/
export type ExperimentalConfig = {
localizeStatus?: boolean
}
export type AfterErrorHook = (
args: AfterErrorHookArgs,
) => AfterErrorResult | Promise<AfterErrorResult>
@@ -752,6 +760,12 @@ export type Config = {
username?: string
}
| false
/**
* Automatically refresh user tokens for users logged into the dashboard
*
* @default false
*/
autoRefresh?: boolean
/** Set account profile picture. Options: gravatar, default or a custom React component. */
avatar?:
| 'default'
@@ -1041,6 +1055,12 @@ export type Config = {
email?: EmailAdapter | Promise<EmailAdapter>
/** Custom REST endpoints */
endpoints?: Endpoint[]
/**
* Configure experimental features for Payload.
*
* These features may be unstable and may change or be removed in future releases.
*/
experimental?: ExperimentalConfig
/**
* Options for folder view within the admin panel
* @experimental this feature may change in minor versions until it is fully stable
@@ -1309,6 +1329,7 @@ export type SanitizedConfig = {
/** Default richtext editor to use for richText fields */
editor?: RichTextAdapter<any, any, any>
endpoints: Endpoint[]
experimental?: ExperimentalConfig
globals: SanitizedGlobalConfig[]
i18n: Required<I18nOptions>
jobs: SanitizedJobsConfig

View File

@@ -390,6 +390,7 @@ export type CreateVersionArgs<T = TypeWithID> = {
autosave: boolean
collectionSlug: CollectionSlug
createdAt: string
localeStatus?: Record<string, 'draft' | 'published'>
/** ID of the parent document for which the version should be created for */
parent: number | string
publishedLocale?: string
@@ -414,6 +415,7 @@ export type CreateGlobalVersionArgs<T = TypeWithID> = {
autosave: boolean
createdAt: string
globalSlug: GlobalSlug
localeStatus?: Record<string, 'draft' | 'published'>
/** ID of the parent document for which the version should be created for */
parent: number | string
publishedLocale?: string

View File

@@ -75,8 +75,6 @@ export {
export { extractID } from '../utilities/extractID.js'
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
export { flattenAllFields } from '../utilities/flattenAllFields.js'
export { flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
export { formatAdminURL } from '../utilities/formatAdminURL.js'

View File

@@ -87,8 +87,8 @@ export type RootFoldersConfiguration = {
collectionOverrides?: (({
collection,
}: {
collection: CollectionConfig
}) => CollectionConfig | Promise<CollectionConfig>)[]
collection: Omit<CollectionConfig, 'trash'>
}) => Omit<CollectionConfig, 'trash'> | Promise<Omit<CollectionConfig, 'trash'>>)[]
/**
* If true, you can scope folders to specific collections.
*

View File

@@ -11,13 +11,18 @@ import { findOneOperation } from '../operations/findOne.js'
export const findOneHandler: PayloadHandler = async (req) => {
const globalConfig = getRequestGlobal(req)
const { searchParams } = req
const depth = searchParams.get('depth')
const { data, searchParams } = req
const depth = data ? data.depth : searchParams.get('depth')
const result = await findOneOperation({
slug: globalConfig.slug,
data: data
? data?.data
: searchParams.get('data')
? JSON.parse(searchParams.get('data') as string)
: undefined,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
draft: data ? data.draft : searchParams.get('draft') === 'true',
globalConfig,
populate: sanitizePopulateParam(req.query.populate),
req,

View File

@@ -17,6 +17,11 @@ import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { replaceWithDraftIfAvailable } from '../../versions/drafts/replaceWithDraftIfAvailable.js'
type Args = {
/**
* You may pass the document data directly which will skip the `db.findOne` database query.
* This is useful if you want to use this endpoint solely for running hooks and populating data.
*/
data?: Record<string, unknown>
depth?: number
draft?: boolean
globalConfig: SanitizedGlobalConfig
@@ -67,13 +72,15 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
// Perform database operation
// /////////////////////////////////////
let doc = await req.payload.db.findGlobal({
slug,
locale: locale!,
req,
select,
where: overrideAccess ? undefined : (accessResult as Where),
})
let doc =
(args.data as any) ??
(await req.payload.db.findGlobal({
slug,
locale: locale!,
req,
select,
where: overrideAccess ? undefined : (accessResult as Where),
}))
if (!doc) {
doc = {}
}

View File

@@ -21,6 +21,11 @@ export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
* to determine if it should run or not.
*/
context?: RequestContext
/**
* You may pass the document data directly which will skip the `db.findOne` database query.
* This is useful if you want to use this endpoint solely for running hooks and populating data.
*/
data?: Record<string, unknown>
/**
* [Control auto-population](https://payloadcms.com/docs/queries/depth) of nested relationship and upload fields.
*/
@@ -84,6 +89,7 @@ export async function findOneGlobalLocal<
): Promise<TransformGlobalWithSelect<TSlug, TSelect>> {
const {
slug: globalSlug,
data,
depth,
draft = false,
includeLockStatus,
@@ -101,6 +107,7 @@ export async function findOneGlobalLocal<
return findOneOperation({
slug: globalSlug as string,
data,
depth,
draft,
globalConfig,

View File

@@ -285,6 +285,7 @@ export const updateOperation = async <
docWithLocales: result,
draft: shouldSaveDraft,
global: globalConfig,
locale,
operation: 'update',
payload,
publishSpecificLocale,

View File

@@ -1671,7 +1671,6 @@ export {
type CustomVersionParser,
} from './utilities/dependencies/dependencyChecker.js'
export { getDependencies } from './utilities/dependencies/getDependencies.js'
export type { FieldSchemaJSON } from './utilities/fieldSchemaToJSON.js'
export {
findUp,
findUpSync,

View File

@@ -1,129 +0,0 @@
import type { ClientConfig } from '../config/client.js'
import type { ClientField } from '../fields/config/client.js'
import type { Field, FieldTypes } from '../fields/config/types.js'
import { fieldAffectsData } from '../fields/config/types.js'
export type FieldSchemaJSON = {
blocks?: FieldSchemaJSON // TODO: conditionally add based on `type`
fields?: FieldSchemaJSON // TODO: conditionally add based on `type`
hasMany?: boolean // TODO: conditionally add based on `type`
name: string
relationTo?: string // TODO: conditionally add based on `type`
slug?: string // TODO: conditionally add based on `type`
type: FieldTypes
}[]
export const fieldSchemaToJSON = (
fields: (ClientField | Field)[],
config: ClientConfig,
): FieldSchemaJSON => {
return fields.reduce((acc, field) => {
let result = acc
switch (field.type) {
case 'array':
acc.push({
name: field.name,
type: field.type,
fields: fieldSchemaToJSON(
[
...field.fields,
{
name: 'id',
type: 'text',
},
],
config,
),
})
break
case 'blocks':
acc.push({
name: field.name,
type: field.type,
blocks: (field.blockReferences ?? field.blocks).reduce((acc, _block) => {
const block = typeof _block === 'string' ? config.blocksMap[_block]! : _block
;(acc as any)[block.slug] = {
fields: fieldSchemaToJSON(
[
...block.fields,
{
name: 'id',
type: 'text',
},
],
config,
),
}
return acc
}, {} as FieldSchemaJSON),
})
break
case 'collapsible': // eslint-disable no-fallthrough
case 'row':
result = result.concat(fieldSchemaToJSON(field.fields, config))
break
case 'group':
if (fieldAffectsData(field)) {
acc.push({
name: field.name,
type: field.type,
fields: fieldSchemaToJSON(field.fields, config),
})
} else {
result = result.concat(fieldSchemaToJSON(field.fields, config))
}
break
case 'relationship': // eslint-disable no-fallthrough
case 'upload':
acc.push({
name: field.name,
type: field.type,
hasMany: 'hasMany' in field ? Boolean(field.hasMany) : false, // TODO: type this
relationTo: field.relationTo as string,
})
break
case 'tabs': {
let tabFields: FieldSchemaJSON = []
field.tabs.forEach((tab) => {
if ('name' in tab) {
tabFields.push({
name: tab.name,
type: field.type,
fields: fieldSchemaToJSON(tab.fields, config),
})
return
}
tabFields = tabFields.concat(fieldSchemaToJSON(tab.fields, config))
})
result = result.concat(tabFields)
break
}
default:
if ('name' in field) {
acc.push({
name: field.name,
type: field.type,
})
}
}
return result
}, [] as FieldSchemaJSON)
}

View File

@@ -84,21 +84,44 @@ export const handleEndpoints = async ({
(request.headers.get('X-Payload-HTTP-Method-Override') === 'GET' ||
request.headers.get('X-HTTP-Method-Override') === 'GET')
) {
const search = await request.text()
let url = request.url
let data: any = undefined
if (request.headers.get('Content-Type') === 'application/x-www-form-urlencoded') {
const search = await request.text()
url = `${request.url}?${search}`
} else if (request.headers.get('Content-Type') === 'application/json') {
// May not be supported by every endpoint
data = await request.json()
// locale and fallbackLocale is read by createPayloadRequest to populate req.locale and req.fallbackLocale
// => add to searchParams
if (data?.locale) {
url += `?locale=${data.locale}`
}
if (data?.fallbackLocale) {
url += `&fallbackLocale=${data.depth}`
}
}
const req = new Request(url, {
// @ts-expect-error // TODO: check if this is required
cache: request.cache,
credentials: request.credentials,
headers: request.headers,
method: 'GET',
signal: request.signal,
})
if (data) {
// @ts-expect-error attach data to request - less overhead than using urlencoded
req.data = data
}
const url = `${request.url}?${new URLSearchParams(search).toString()}`
const response = await handleEndpoints({
basePath,
config: incomingConfig,
path,
request: new Request(url, {
// @ts-expect-error // TODO: check if this is required
cache: request.cache,
credentials: request.credentials,
headers: request.headers,
method: 'GET',
signal: request.signal,
}),
request: req,
})
return response

View File

@@ -17,6 +17,7 @@ export const headersWithCors = ({ headers, req }: CorsArgs): Headers => {
'Authorization',
'Content-Encoding',
'x-apollo-tracing',
'X-Payload-HTTP-Method-Override',
]
headers.set('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')

View File

@@ -1,4 +1,5 @@
// @ts-strict-ignore
import type { SanitizedConfig } from '../config/types.js'
import type { CheckboxField, Field, Option } from '../fields/config/types.js'
export const statuses: Option[] = [
@@ -43,3 +44,23 @@ export const versionSnapshotField: CheckboxField = {
},
index: true,
}
export function buildLocaleStatusField(config: SanitizedConfig): Field[] {
if (!config.localization || !config.localization.locales) {
return []
}
return config.localization.locales.map((locale) => {
const code = typeof locale === 'string' ? locale : locale.code
return {
name: code,
type: 'select',
index: true,
options: [
{ label: ({ t }) => t('version:draft'), value: 'draft' },
{ label: ({ t }) => t('version:published'), value: 'published' },
],
}
})
}

View File

@@ -2,7 +2,7 @@ import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { Field, FlattenedField } from '../fields/config/types.js'
import { versionSnapshotField } from './baseFields.js'
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
export const buildVersionCollectionFields = <T extends boolean = false>(
config: SanitizedConfig,
@@ -62,6 +62,23 @@ export const buildVersionCollectionFields = <T extends boolean = false>(
return locale.code
}),
})
if (config.experimental?.localizeStatus) {
const localeStatusFields = buildLocaleStatusField(config)
fields.push({
name: 'localeStatus',
type: 'group',
admin: {
disableBulkEdit: true,
disabled: true,
},
fields: localeStatusFields,
...(flatten && {
flattenedFields: localeStatusFields as FlattenedField[],
})!,
})
}
}
fields.push({

View File

@@ -2,7 +2,7 @@ import type { SanitizedConfig } from '../config/types.js'
import type { Field, FlattenedField } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import { versionSnapshotField } from './baseFields.js'
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
export const buildVersionGlobalFields = <T extends boolean = false>(
config: SanitizedConfig,
@@ -56,6 +56,23 @@ export const buildVersionGlobalFields = <T extends boolean = false>(
return locale.code
}),
})
if (config.experimental.localizeStatus) {
const localeStatusFields = buildLocaleStatusField(config)
fields.push({
name: 'localeStatus',
type: 'group',
admin: {
disableBulkEdit: true,
disabled: true,
},
fields: localeStatusFields,
...(flatten && {
flattenedFields: localeStatusFields as FlattenedField[],
})!,
})
}
}
fields.push({

View File

@@ -111,6 +111,12 @@ export const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
draft.version = {} as T
}
// Lift locale status from version data if available
const localeStatus = draft.localeStatus || {}
if (locale && localeStatus[locale]) {
;(draft.version as { _status?: string })['_status'] = localeStatus[locale]
}
// Disregard all other draft content at this point,
// Only interested in the version itself.
// Operations will handle firing hooks, etc.

View File

@@ -1,3 +1,5 @@
import { version } from 'os'
// @ts-strict-ignore
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
@@ -16,6 +18,7 @@ type Args = {
draft?: boolean
global?: SanitizedGlobalConfig
id?: number | string
locale?: null | string
operation?: 'create' | 'restoreVersion' | 'update'
payload: Payload
publishSpecificLocale?: string
@@ -31,6 +34,7 @@ export const saveVersion = async ({
docWithLocales: doc,
draft,
global,
locale,
operation,
payload,
publishSpecificLocale,
@@ -42,6 +46,7 @@ export const saveVersion = async ({
let createNewVersion = true
const now = new Date().toISOString()
const versionData = deepCopyObjectSimple(doc)
if (draft) {
versionData._status = 'draft'
}
@@ -55,39 +60,39 @@ export const saveVersion = async ({
}
try {
if (autosave) {
let docs
const findVersionArgs = {
let docs
const findVersionArgs = {
limit: 1,
pagination: false,
req,
sort: '-updatedAt',
}
if (collection) {
;({ docs } = await payload.db.findVersions({
...findVersionArgs,
collection: collection.slug,
limit: 1,
pagination: false,
req,
sort: '-updatedAt',
}
if (collection) {
;({ docs } = await payload.db.findVersions({
...findVersionArgs,
collection: collection.slug,
limit: 1,
pagination: false,
req,
where: {
parent: {
equals: id,
},
where: {
parent: {
equals: id,
},
}))
} else {
;({ docs } = await payload.db.findGlobalVersions({
...findVersionArgs,
global: global!.slug,
limit: 1,
pagination: false,
req,
}))
}
const [latestVersion] = docs
},
}))
} else {
;({ docs } = await payload.db.findGlobalVersions({
...findVersionArgs,
global: global!.slug,
limit: 1,
pagination: false,
req,
}))
}
const [latestVersion] = docs
if (autosave) {
// overwrite the latest version if it's set to autosave
if (latestVersion && 'autosave' in latestVersion && latestVersion.autosave === true) {
createNewVersion = false
@@ -125,11 +130,53 @@ export const saveVersion = async ({
}
if (createNewVersion) {
let localeStatus = {}
const localizationEnabled =
payload.config.localization && payload.config.localization.locales.length > 0
if (
localizationEnabled &&
payload.config.localization !== false &&
payload.config.experimental?.localizeStatus
) {
const allLocales = (
(payload.config.localization && payload.config.localization?.locales) ||
[]
).map((locale) => (typeof locale === 'string' ? locale : locale.code))
// If `publish all`, set all locales to published
if (versionData._status === 'published' && !publishSpecificLocale) {
localeStatus = Object.fromEntries(allLocales.map((code) => [code, 'published']))
} else if (publishSpecificLocale || (locale && versionData._status === 'draft')) {
const status: 'draft' | 'published' = publishSpecificLocale ? 'published' : 'draft'
const incomingLocale = String(publishSpecificLocale || locale)
const existing = latestVersion?.localeStatus
// If no locale statuses are set, set it and set all others to draft
if (!existing) {
localeStatus = {
...Object.fromEntries(
allLocales.filter((code) => code !== incomingLocale).map((code) => [code, 'draft']),
),
[incomingLocale]: status,
}
} else {
// If locales already exist, update the status for the incoming locale
const { [incomingLocale]: _, ...rest } = existing
localeStatus = {
...rest,
[incomingLocale]: status,
}
}
}
}
const createVersionArgs = {
autosave: Boolean(autosave),
collectionSlug: undefined as string | undefined,
createdAt: operation === 'restoreVersion' ? versionData.createdAt : now,
globalSlug: undefined as string | undefined,
localeStatus,
parent: collection ? id : undefined,
publishedLocale: publishSpecificLocale || undefined,
req,

View File

@@ -122,6 +122,7 @@ export type SanitizedGlobalVersions = {
export type TypeWithVersion<T> = {
createdAt: string
id: string
localeStatus: Record<string, 'draft' | 'published'>
parent: number | string
publishedLocale?: string
snapshot?: boolean

View File

@@ -183,6 +183,11 @@
"types": "./src/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.ts",
"default": "./src/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.ts"
},
"./lexical/react/LexicalNodeContextMenuPlugin": {
"import": "./src/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.ts",
"types": "./src/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.ts",
"default": "./src/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.ts"
},
"./lexical/react/LexicalDecoratorBlockNode": {
"import": "./src/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.ts",
"types": "./src/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.ts",
@@ -360,16 +365,16 @@
]
},
"dependencies": {
"@lexical/headless": "0.28.0",
"@lexical/html": "0.28.0",
"@lexical/link": "0.28.0",
"@lexical/list": "0.28.0",
"@lexical/mark": "0.28.0",
"@lexical/react": "0.28.0",
"@lexical/rich-text": "0.28.0",
"@lexical/selection": "0.28.0",
"@lexical/table": "0.28.0",
"@lexical/utils": "0.28.0",
"@lexical/headless": "0.35.0",
"@lexical/html": "0.35.0",
"@lexical/link": "0.35.0",
"@lexical/list": "0.35.0",
"@lexical/mark": "0.35.0",
"@lexical/react": "0.35.0",
"@lexical/rich-text": "0.35.0",
"@lexical/selection": "0.35.0",
"@lexical/table": "0.35.0",
"@lexical/utils": "0.35.0",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/uuid": "10.0.0",
@@ -379,7 +384,7 @@
"dequal": "2.0.3",
"escape-html": "1.0.3",
"jsox": "1.2.121",
"lexical": "0.28.0",
"lexical": "0.35.0",
"mdast-util-from-markdown": "2.0.2",
"mdast-util-mdx-jsx": "3.1.3",
"micromark-extension-mdx-jsx": "3.0.1",
@@ -394,7 +399,7 @@
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@lexical/eslint-plugin": "0.28.0",
"@lexical/eslint-plugin": "0.35.0",
"@payloadcms/eslint-config": "workspace:*",
"@types/escape-html": "1.0.4",
"@types/json-schema": "7.0.15",
@@ -581,6 +586,11 @@
"types": "./dist/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.d.ts",
"default": "./dist/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.js"
},
"./lexical/react/LexicalNodeContextMenuPlugin": {
"import": "./dist/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.js",
"types": "./dist/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.d.ts",
"default": "./dist/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.js"
},
"./lexical/react/LexicalDecoratorBlockNode": {
"import": "./dist/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.js",
"types": "./dist/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.d.ts",

View File

@@ -8,17 +8,18 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { useLexicalEditable } from '@lexical/react/useLexicalEditable'
import {
$computeTableMapSkipCellCheck,
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$deleteTableColumnAtSelection,
$deleteTableRowAtSelection,
$getNodeTriplet,
$getTableCellNodeFromLexicalNode,
$getTableColumnIndexFromTableCellNode,
$getTableNodeFromLexicalNodeOrThrow,
$getTableRowIndexFromTableCellNode,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$insertTableColumnAtSelection,
$insertTableRowAtSelection,
$isTableCellNode,
$isTableSelection,
$mergeCells,
$unmergeCell,
getTableElement,
getTableObserverFromTableElement,
@@ -28,11 +29,8 @@ import {
import { mergeRegister } from '@lexical/utils'
import { useScrollInfo } from '@payloadcms/ui'
import {
$createParagraphNode,
$getRoot,
$getSelection,
$isElementNode,
$isParagraphNode,
$isRangeSelection,
$isTextNode,
$setSelection,
@@ -74,17 +72,6 @@ function $canUnmerge(): boolean {
return cell.__colSpan > 1 || cell.__rowSpan > 1
}
function $cellContainsEmptyParagraph(cell: TableCellNode): boolean {
if (cell.getChildrenSize() !== 1) {
return false
}
const firstChild = cell.getFirstChildOrThrow()
if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
return false
}
return true
}
function $selectLastDescendant(node: ElementNode): void {
const lastDescendant = node.getLastDescendant()
if ($isTextNode(lastDescendant)) {
@@ -227,105 +214,14 @@ function TableActionMenu({
const mergeTableCellsAtSelection = () => {
editor.update(() => {
const selection = $getSelection()
if ($isTableSelection(selection)) {
// Get all selected cells and compute the total area
const nodes = selection.getNodes()
const tableCells = nodes.filter($isTableCellNode)
if (tableCells.length === 0) {
return
}
// Find the table node
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCells[0] as TableCellNode)
const [gridMap] = $computeTableMapSkipCellCheck(tableNode, null, null)
// Find the boundaries of the selection including merged cells
let minRow = Infinity
let maxRow = -Infinity
let minCol = Infinity
let maxCol = -Infinity
// First pass: find the actual boundaries considering merged cells
const processedCells = new Set()
for (const row of gridMap) {
for (const mapCell of row) {
if (!mapCell || !mapCell.cell) {
continue
}
const cellKey = mapCell.cell.getKey()
if (processedCells.has(cellKey)) {
continue
}
if (tableCells.some((cell) => cell.is(mapCell.cell))) {
processedCells.add(cellKey)
// Get the actual position of this cell in the grid
const cellStartRow = mapCell.startRow
const cellStartCol = mapCell.startColumn
const cellRowSpan = mapCell.cell.__rowSpan || 1
const cellColSpan = mapCell.cell.__colSpan || 1
// Update boundaries considering the cell's actual position and span
minRow = Math.min(minRow, cellStartRow)
maxRow = Math.max(maxRow, cellStartRow + cellRowSpan - 1)
minCol = Math.min(minCol, cellStartCol)
maxCol = Math.max(maxCol, cellStartCol + cellColSpan - 1)
}
}
}
// Validate boundaries
if (minRow === Infinity || minCol === Infinity) {
return
}
// The total span of the merged cell
const totalRowSpan = maxRow - minRow + 1
const totalColSpan = maxCol - minCol + 1
// Use the top-left cell as the target cell
const targetCellMap = gridMap?.[minRow]?.[minCol]
if (!targetCellMap?.cell) {
return
}
const targetCell = targetCellMap.cell
// Set the spans for the target cell
targetCell.setColSpan(totalColSpan)
targetCell.setRowSpan(totalRowSpan)
// Move content from other cells to the target cell
const seenCells = new Set([targetCell.getKey()])
// Second pass: merge content and remove other cells
for (let row = minRow; row <= maxRow; row++) {
for (let col = minCol; col <= maxCol; col++) {
const mapCell = gridMap?.[row]?.[col]
if (!mapCell?.cell) {
continue
}
const currentCell = mapCell.cell
const key = currentCell.getKey()
if (!seenCells.has(key)) {
seenCells.add(key)
const isEmpty = $cellContainsEmptyParagraph(currentCell)
if (!isEmpty) {
targetCell.append(...currentCell.getChildren())
}
currentCell.remove()
}
}
}
// Ensure target cell has content
if (targetCell.getChildrenSize() === 0) {
targetCell.append($createParagraphNode())
}
if (!$isTableSelection(selection)) {
return
}
const nodes = selection.getNodes()
const tableCells = nodes.filter($isTableCellNode)
const targetCell = $mergeCells(tableCells)
if (targetCell) {
$selectLastDescendant(targetCell)
onClose()
}
@@ -342,7 +238,7 @@ function TableActionMenu({
(shouldInsertAfter: boolean) => {
editor.update(() => {
for (let i = 0; i < selectionCounts.rows; i++) {
$insertTableRow__EXPERIMENTAL(shouldInsertAfter)
$insertTableRowAtSelection(shouldInsertAfter)
}
onClose()
})
@@ -354,7 +250,7 @@ function TableActionMenu({
(shouldInsertAfter: boolean) => {
editor.update(() => {
for (let i = 0; i < selectionCounts.columns; i++) {
$insertTableColumn__EXPERIMENTAL(shouldInsertAfter)
$insertTableColumnAtSelection(shouldInsertAfter)
}
onClose()
})
@@ -364,7 +260,7 @@ function TableActionMenu({
const deleteTableRowAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableRow__EXPERIMENTAL()
$deleteTableRowAtSelection()
onClose()
})
}, [editor, onClose])
@@ -381,7 +277,7 @@ function TableActionMenu({
const deleteTableColumnAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableColumn__EXPERIMENTAL()
$deleteTableColumnAtSelection()
onClose()
})
}, [editor, onClose])

View File

@@ -17,7 +17,7 @@ import {
TableNode,
} from '@lexical/table'
import { calculateZoomLevel, mergeRegister } from '@lexical/utils'
import { $getNearestNodeFromDOMNode, isHTMLElement } from 'lexical'
import { $getNearestNodeFromDOMNode, isHTMLElement, SKIP_SCROLL_INTO_VIEW_TAG } from 'lexical'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
@@ -223,7 +223,7 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
const newHeight = Math.max(height + heightChange, MIN_ROW_HEIGHT)
tableRow.setHeight(newHeight)
},
{ tag: 'skip-scroll-into-view' },
{ tag: SKIP_SCROLL_INTO_VIEW_TAG },
)
},
[activeCell, editor],
@@ -281,7 +281,7 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
newColWidths[columnIndex] = newWidth
tableNode.setColWidths(newColWidths)
},
{ tag: 'skip-scroll-into-view' },
{ tag: SKIP_SCROLL_INTO_VIEW_TAG },
)
},
[activeCell, editor],

View File

@@ -9,8 +9,8 @@ import {
$getTableAndElementByKey,
$getTableColumnIndexFromTableCellNode,
$getTableRowIndexFromTableCellNode,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$insertTableColumnAtSelection,
$insertTableRowAtSelection,
$isTableCellNode,
$isTableNode,
getTableElement,
@@ -223,10 +223,10 @@ function TableHoverActionsContainer({
const maybeTableNode = $getNearestNodeFromDOMNode(tableCellDOMNodeRef.current)
maybeTableNode?.selectEnd()
if (insertRow) {
$insertTableRow__EXPERIMENTAL()
$insertTableRowAtSelection()
setShownRow(false)
} else {
$insertTableColumn__EXPERIMENTAL()
$insertTableColumnAtSelection()
setShownColumn(false)
}
}

View File

@@ -29,7 +29,7 @@ const toolbarGroups = (props: TextStateFeatureProps): ToolbarGroup[] => {
{
ChildComponent: () => <TextStateIcon />,
key: `clear-style`,
label: 'Default style',
label: ({ i18n }) => i18n.t('lexical:textState:defaultStyle'),
onSelect: ({ editor }) => {
for (const stateKey in props.state) {
setTextState(editor, stateKey, undefined)

View File

@@ -2,6 +2,7 @@ import type { PropertiesHyphenFallback } from 'csstype'
import type { Prettify } from 'ts-essentials'
import { createServerFeature } from '../../utilities/createServerFeature.js'
import { i18n } from './i18n.js'
// extracted from https://github.com/facebook/lexical/pull/7294
export type StyleObject = Prettify<{
@@ -71,6 +72,7 @@ export const TextStateFeature = createServerFeature<
clientFeatureProps: {
state: props?.state,
},
i18n,
}
},
key: 'textState',

View File

@@ -0,0 +1,39 @@
import type { GenericLanguages } from '@payloadcms/translations'
export const i18n: Partial<GenericLanguages> = {
ar: { defaultStyle: 'النمط الافتراضي' },
az: { defaultStyle: 'Defolt üslub' },
bg: { defaultStyle: 'Стандартен стил' },
cs: { defaultStyle: 'Výchozí styl' },
da: { defaultStyle: 'Standardstil' },
de: { defaultStyle: 'Standardstil' },
en: { defaultStyle: 'Default style' },
es: { defaultStyle: 'Estilo predeterminado' },
et: { defaultStyle: 'Vaikimisi stiil' },
fa: { defaultStyle: 'سبک پیش‌فرض' },
fr: { defaultStyle: 'Style par défaut' },
he: { defaultStyle: 'סגנון ברירת מחדל' },
hr: { defaultStyle: 'Zadani stil' },
hu: { defaultStyle: 'Alapértelmezett stílus' },
it: { defaultStyle: 'Stile predefinito' },
ja: { defaultStyle: 'デフォルトスタイル' },
ko: { defaultStyle: '기본 스타일' },
my: { defaultStyle: 'Gaya lalai' },
nb: { defaultStyle: 'Standardstil' },
nl: { defaultStyle: 'Standaardstijl' },
pl: { defaultStyle: 'Domyślny styl' },
pt: { defaultStyle: 'Estilo padrão' },
ro: { defaultStyle: 'Stil implicit' },
rs: { defaultStyle: 'Подразумевани стил' },
'rs-latin': { defaultStyle: 'Podrazumevani stil' },
ru: { defaultStyle: 'Стиль по умолчанию' },
sk: { defaultStyle: 'Predvolený štýl' },
sl: { defaultStyle: 'Privzeti slog' },
sv: { defaultStyle: 'Standardstil' },
th: { defaultStyle: 'สไตล์เริ่มต้น' },
tr: { defaultStyle: 'Varsayılan stil' },
uk: { defaultStyle: 'Стиль за замовчуванням' },
vi: { defaultStyle: 'Kiểu mặc định' },
zh: { defaultStyle: '默认样式' },
'zh-TW': { defaultStyle: '預設樣式' },
}

View File

@@ -26,7 +26,7 @@ import { richTextValidateHOC } from './validate/index.js'
let checkedDependencies = false
export const lexicalTargetVersion = '0.28.0'
export const lexicalTargetVersion = '0.35.0'
export function lexicalEditor(args?: LexicalEditorProps): LexicalRichTextAdapterProvider {
if (

View File

@@ -0,0 +1 @@
export * from '@lexical/react/LexicalNodeContextMenuPlugin'

View File

@@ -4,6 +4,17 @@
.rich-text-lexical {
.editor {
position: relative;
&[data-rtl='true'] {
direction: rtl;
text-align: right;
// Every single lexical node needs the `direction: rtl` class. This allows
// $isParentRTL() to work correctly.
* {
direction: rtl;
}
}
}
.editor-container {

View File

@@ -4,6 +4,7 @@ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary.js'
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin.js'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin.js'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin.js'
import { isFieldRTL, useConfig, useLocale } from '@payloadcms/ui'
import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical'
import * as React from 'react'
import { useEffect, useState } from 'react'
@@ -11,8 +12,8 @@ import { useEffect, useState } from 'react'
import type { LexicalProviderProps } from './LexicalProvider.js'
import { useEditorConfigContext } from './config/client/EditorConfigProvider.js'
import { EditorPlugin } from './EditorPlugin.js'
import './LexicalEditor.scss'
import { EditorPlugin } from './EditorPlugin.js'
import { DecoratorPlugin } from './plugins/DecoratorPlugin/index.js'
import { AddBlockHandlePlugin } from './plugins/handles/AddBlockHandlePlugin/index.js'
import { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin/index.js'
@@ -28,9 +29,9 @@ export const LexicalEditor: React.FC<
{
editorContainerRef: React.RefObject<HTMLDivElement | null>
isSmallWidthViewport: boolean
} & Pick<LexicalProviderProps, 'editorConfig' | 'onChange'>
} & Pick<LexicalProviderProps, 'editorConfig' | 'fieldProps' | 'onChange'>
> = (props) => {
const { editorConfig, editorContainerRef, isSmallWidthViewport, onChange } = props
const { editorConfig, editorContainerRef, fieldProps, isSmallWidthViewport, onChange } = props
const editorConfigContext = useEditorConfigContext()
const [editor] = useLexicalComposerContext()
@@ -82,6 +83,15 @@ export const LexicalEditor: React.FC<
editorConfigContext.parentEditor?.unregisterChild?.(editorConfigContext.uuid)
}
}, [editor, editorConfigContext])
const locale = useLocale()
const { config } = useConfig()
const rtl = isFieldRTL({
fieldLocalized: !!fieldProps?.field?.localized,
fieldRTL: undefined,
locale,
localizationConfig: config.localization || undefined,
})
return (
<React.Fragment>
@@ -101,7 +111,7 @@ export const LexicalEditor: React.FC<
<RichTextPlugin
contentEditable={
<div className="editor-scroller">
<div className="editor" ref={onRef}>
<div className="editor" data-rtl={rtl} ref={onRef}>
<LexicalContentEditable editorConfig={editorConfig} />
</div>
</div>

View File

@@ -115,6 +115,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
<LexicalEditorComponent
editorConfig={editorConfig}
editorContainerRef={editorContainerRef}
fieldProps={fieldProps}
isSmallWidthViewport={isSmallWidthViewport}
onChange={onChange}
/>

View File

@@ -2,6 +2,7 @@
@layer payload-default {
.draggable-block-menu {
border: none;
border-radius: $style-radius-m;
padding: 0;
cursor: grab;
@@ -24,6 +25,7 @@
}
.icon {
background-color: transparent;
width: 18px;
height: 24px;
opacity: 0.3;

View File

@@ -70,7 +70,7 @@ function useDraggableBlockMenu(
): React.ReactElement {
const scrollerElem = anchorElem.parentElement
const menuRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLButtonElement>(null)
const targetLineRef = useRef<HTMLDivElement>(null)
const debugHighlightRef = useRef<HTMLDivElement>(null)
const isDraggingBlockRef = useRef<boolean>(false)
@@ -396,7 +396,7 @@ function useDraggableBlockMenu(
editorConfig?.admin?.hideGutter,
])
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
function onDragStart(event: ReactDragEvent<HTMLButtonElement>): void {
const dataTransfer = event.dataTransfer
if (!dataTransfer || !draggableBlockElem) {
return
@@ -422,15 +422,17 @@ function useDraggableBlockMenu(
return createPortal(
<React.Fragment>
<div
<button
aria-label="Drag to move"
className="icon draggable-block-menu"
draggable
onDragEnd={onDragEnd}
onDragStart={onDragStart}
ref={menuRef}
type="button"
>
<div className={isEditable ? 'icon' : ''} />
</div>
</button>
<div className="draggable-block-target-line" ref={targetLineRef} />
<div className="debug-highlight" ref={debugHighlightRef} />
</React.Fragment>,

View File

@@ -2,14 +2,6 @@
@layer payload-default {
.LexicalEditorTheme {
&__ltr {
text-align: left;
}
&__rtl {
text-align: right;
}
&__paragraph {
font-size: base(0.8);
margin-bottom: 0.55em;
@@ -32,6 +24,15 @@
border-inline-start-style: solid;
padding-inline-start: base(0.6);
padding-block: base(0.2);
[data-rtl='true'] & {
border-inline-start: none;
border-inline-end-color: var(--theme-elevation-150);
border-inline-end-width: base(0.2);
border-inline-end-style: solid;
padding-inline-start: 0;
padding-inline-end: base(0.6);
}
}
&__h1 {

View File

@@ -134,13 +134,13 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
if (collection && id) {
entitySlug = collection.slug
url = `${serverURL}${api}/${entitySlug}/${id}?draft=true&autosave=true&locale=${localeRef.current}`
url = `${serverURL}${api}/${entitySlug}/${id}?depth=0&draft=true&autosave=true&locale=${localeRef.current}`
method = 'PATCH'
}
if (globalDoc) {
entitySlug = globalDoc.slug
url = `${serverURL}${api}/globals/${entitySlug}?draft=true&autosave=true&locale=${localeRef.current}`
url = `${serverURL}${api}/globals/${entitySlug}?depth=0&draft=true&autosave=true&locale=${localeRef.current}`
method = 'POST'
}

View File

@@ -93,7 +93,7 @@
}
position: var(--sidebar-wrap-position, sticky);
top: var(--sidebar-wrap-top, 0);
top: var(--sidebar-wrap-top, var(--doc-controls-height));
width: var(--sidebar-wrap-width, 33.33%);
height: var(--sidebar-wrap-height, calc(100vh - var(--doc-controls-height)));
min-width: var(--sidebar-wrap-min-width, var(--doc-sidebar-width));

View File

@@ -15,17 +15,9 @@ export const getTextFieldsToBeSearched = (
moveSubFieldsToTop: true,
}) as ClientField[]
const searchableFieldNames = new Set(listSearchableFields)
const matchingFields: typeof flattenedFields = []
for (const field of flattenedFields) {
if (fieldAffectsData(field) && searchableFieldNames.has(field.name)) {
matchingFields.push(field)
searchableFieldNames.delete(field.name)
}
}
return matchingFields
return flattenedFields.filter(
(field) => fieldAffectsData(field) && listSearchableFields.includes(field.name),
)
}
return null

View File

@@ -6,101 +6,21 @@
flex-direction: column;
gap: 2px;
&__wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: base(0.5);
background-color: var(--theme-elevation-50);
border-radius: var(--style-radius-m);
}
&__search {
display: flex;
background-color: var(--theme-elevation-50);
border-radius: var(--style-radius-m);
gap: base(0.4);
flex-grow: 1;
position: relative;
.icon {
flex-shrink: 0;
}
}
.icon--search {
position: absolute;
left: 0;
top: 50%;
transform: translate3d(0, -50%, 0);
inset-inline-start: base(0.4);
z-index: 1;
pointer-events: none;
}
.search-filter {
flex-grow: 1;
input {
height: 46px;
padding-left: 36px;
margin: 0;
}
}
&__custom-control {
padding: 0;
border-radius: 0;
}
&__buttons {
display: flex;
align-items: center;
gap: calc(var(--base) / 4);
padding-right: 10px;
}
.pill-selector,
.where-builder,
.sort-complex,
.group-by-builder {
margin-top: base(1);
margin-top: calc(var(--base) / 2);
}
@include small-break {
&__wrap {
flex-direction: column;
align-items: stretch;
background-color: transparent;
border-radius: 0;
}
.search-filter {
width: 100%;
input {
height: 40px;
padding: 0 base(1.5);
}
}
&__buttons {
padding-right: 0;
margin: 0;
width: 100%;
.search-bar__actions {
.pill {
padding: base(0.2) base(0.2) base(0.2) base(0.4);
justify-content: space-between;
}
}
.pill-selector,
.where-builder,
.sort-complex {
margin-top: calc(var(--base) / 2);
}
&__toggle-columns,
&__toggle-where,
&__toggle-sort,

View File

@@ -11,7 +11,6 @@ import { Popup } from '../../elements/Popup/index.js'
import { useUseTitleField } from '../../hooks/useUseAsTitle.js'
import { ChevronIcon } from '../../icons/Chevron/index.js'
import { Dots } from '../../icons/Dots/index.js'
import { SearchIcon } from '../../icons/Search/index.js'
import { useListQuery } from '../../providers/ListQuery/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { AnimateHeight } from '../AnimateHeight/index.js'
@@ -19,7 +18,7 @@ import { ColumnSelector } from '../ColumnSelector/index.js'
import { GroupByBuilder } from '../GroupByBuilder/index.js'
import { Pill } from '../Pill/index.js'
import { QueryPresetBar } from '../QueryPresets/QueryPresetBar/index.js'
import { SearchFilter } from '../SearchFilter/index.js'
import { SearchBar } from '../SearchBar/index.js'
import { WhereBuilder } from '../WhereBuilder/index.js'
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
import './index.scss'
@@ -132,67 +131,64 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
queryPresetPermissions={queryPresetPermissions}
/>
)}
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__search`}>
<SearchIcon />
<SearchFilter
handleChange={handleSearchChange}
key={collectionSlug}
label={searchLabelTranslated.current}
searchQueryParam={query?.search}
/>
</div>
<div className={`${baseClass}__buttons`}>
{!smallBreak && <React.Fragment>{beforeActions && beforeActions}</React.Fragment>}
{enableColumns && (
<SearchBar
Actions={[
!smallBreak && (
<React.Fragment key="before-actions">{beforeActions && beforeActions}</React.Fragment>
),
enableColumns && (
<Pill
aria-controls={`${baseClass}-columns`}
aria-expanded={visibleDrawer === 'columns'}
className={`${baseClass}__toggle-columns`}
icon={<ChevronIcon direction={visibleDrawer === 'columns' ? 'up' : 'down'} />}
id="toggle-list-columns"
key="toggle-list-columns"
onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)}
pillStyle="light"
size="small"
>
{t('general:columns')}
</Pill>
)}
{enableFilters && (
),
enableFilters && (
<Pill
aria-controls={`${baseClass}-where`}
aria-expanded={visibleDrawer === 'where'}
className={`${baseClass}__toggle-where`}
icon={<ChevronIcon direction={visibleDrawer === 'where' ? 'up' : 'down'} />}
id="toggle-list-filters"
key="toggle-list-filters"
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)}
pillStyle="light"
size="small"
>
{t('general:filters')}
</Pill>
)}
{enableSort && (
),
enableSort && (
<Pill
aria-controls={`${baseClass}-sort`}
aria-expanded={visibleDrawer === 'sort'}
className={`${baseClass}__toggle-sort`}
icon={<ChevronIcon />}
id="toggle-list-sort"
key="toggle-list-sort"
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
pillStyle="light"
size="small"
>
{t('general:sort')}
</Pill>
)}
{collectionConfig.admin.groupBy && (
),
collectionConfig.admin.groupBy && (
<Pill
aria-controls={`${baseClass}-group-by`}
aria-expanded={visibleDrawer === 'group-by'}
className={`${baseClass}__toggle-group-by`}
icon={<ChevronIcon direction={visibleDrawer === 'group-by' ? 'up' : 'down'} />}
id="toggle-group-by"
key="toggle-group-by"
onClick={() =>
setVisibleDrawer(visibleDrawer !== 'group-by' ? 'group-by' : undefined)
}
@@ -203,13 +199,14 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
label: '',
})}
</Pill>
)}
{listMenuItems && Array.isArray(listMenuItems) && listMenuItems.length > 0 && (
),
listMenuItems && Array.isArray(listMenuItems) && listMenuItems.length > 0 && (
<Popup
button={<Dots ariaLabel={t('general:moreOptions')} />}
className={`${baseClass}__popup`}
horizontalAlign="right"
id="list-menu"
key="list-menu"
size="small"
verticalAlign="bottom"
>
@@ -217,9 +214,13 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
<Fragment key={`list-menu-item-${i}`}>{item}</Fragment>
))}
</Popup>
)}
</div>
</div>
),
].filter(Boolean)}
key={collectionSlug}
label={searchLabelTranslated.current}
onSearchChange={handleSearchChange}
searchQueryParam={query?.search}
/>
{enableColumns && (
<AnimateHeight
className={`${baseClass}__columns`}

View File

@@ -7,13 +7,14 @@ import React, { useEffect } from 'react'
import { useAllFormFields } from '../../../forms/Form/context.js'
import { useDocumentEvents } from '../../../providers/DocumentEvents/index.js'
import { useDocumentInfo } from '../../../providers/DocumentInfo/index.js'
import { useLivePreviewContext } from '../../../providers/LivePreview/context.js'
import { useLocale } from '../../../providers/Locale/index.js'
import { ShimmerEffect } from '../../ShimmerEffect/index.js'
import { DeviceContainer } from '../Device/index.js'
import './index.scss'
import { IFrame } from '../IFrame/index.js'
import { LivePreviewToolbar } from '../Toolbar/index.js'
import './index.scss'
const baseClass = 'live-preview-window'
@@ -21,7 +22,6 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
const {
appIsReady,
breakpoint,
fieldSchemaJSON,
iframeHasLoaded,
iframeRef,
isLivePreviewing,
@@ -35,10 +35,8 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
const { mostRecentUpdate } = useDocumentEvents()
const prevWindowType =
React.useRef<ReturnType<typeof useLivePreviewContext>['previewWindowType']>(undefined)
const [formState] = useAllFormFields()
const { id, collectionSlug, globalSlug } = useDocumentInfo()
// For client-side apps, send data through `window.postMessage`
// The preview could either be an iframe embedded on the page
@@ -53,20 +51,16 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
if (formState && window && 'postMessage' in window && appIsReady) {
const values = reduceFieldsToValues(formState, true)
// To reduce on large `postMessage` payloads, only send `fieldSchemaToJSON` one time
// To do this, the underlying JS function maintains a cache of this value
// So we need to send it through each time the window type changes
// But only once per window type change, not on every render, because this is a potentially large obj
const shouldSendSchema =
!prevWindowType.current || prevWindowType.current !== previewWindowType
prevWindowType.current = previewWindowType
if (!values.id) {
values.id = id
}
const message = {
type: 'payload-live-preview',
collectionSlug,
data: values,
externallyUpdatedRelationship: mostRecentUpdate,
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
globalSlug,
locale: locale.code,
}
@@ -83,13 +77,15 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
}, [
formState,
url,
collectionSlug,
globalSlug,
iframeHasLoaded,
id,
previewWindowType,
popupRef,
appIsReady,
iframeRef,
setIframeHasLoaded,
fieldSchemaJSON,
mostRecentUpdate,
locale,
isLivePreviewing,

View File

@@ -139,20 +139,22 @@ export function PublishButton({ label: labelProp }: PublishButtonClientProps) {
}
})
const publish = useCallback(() => {
const publish = useCallback(async () => {
if (uploadStatus === 'uploading') {
return
}
void submit({
const result = await submit({
overrides: {
_status: 'published',
},
})
setUnpublishedVersionCount(0)
setMostRecentVersionIsAutosaved(false)
setHasPublishedDoc(true)
if (result) {
setUnpublishedVersionCount(0)
setMostRecentVersionIsAutosaved(false)
setHasPublishedDoc(true)
}
}, [
setHasPublishedDoc,
submit,
@@ -162,7 +164,7 @@ export function PublishButton({ label: labelProp }: PublishButtonClientProps) {
])
const publishSpecificLocale = useCallback(
(locale) => {
async (locale) => {
if (uploadStatus === 'uploading') {
return
}
@@ -175,14 +177,16 @@ export function PublishButton({ label: labelProp }: PublishButtonClientProps) {
globalSlug ? `/globals/${globalSlug}` : `/${collectionSlug}/${id ? `${'/' + id}` : ''}`
}${params ? '?' + params : ''}`
void submit({
const result = await submit({
action,
overrides: {
_status: 'published',
},
})
setHasPublishedDoc(true)
if (result) {
setHasPublishedDoc(true)
}
},
[api, collectionSlug, globalSlug, id, serverURL, setHasPublishedDoc, submit, uploadStatus],
)

View File

@@ -2,18 +2,41 @@
@layer payload-default {
.search-bar {
--icon-width: 40px;
--search-bg: var(--theme-elevation-50);
display: grid;
grid-template-columns: auto 1fr;
width: 100%;
display: flex;
align-items: center;
background-color: var(--theme-elevation-50);
background-color: var(--search-bg);
border-radius: var(--style-radius-m);
padding: calc(var(--base) * 0.6);
gap: calc(var(--base) * 0.6);
position: relative;
min-height: 46px;
isolation: isolate;
&:has(.search-bar__actions) {
grid-template-columns: auto 1fr auto;
}
.icon--search {
grid-column: 1/2;
grid-row: 1/2;
z-index: 1;
align-self: center;
justify-self: center;
pointer-events: none;
width: 40px;
}
.search-filter {
flex-grow: 1;
grid-column: 1/3;
grid-row: 1/2;
background-color: transparent;
border-radius: inherit;
input {
flex-grow: 1;
height: 100%;
padding: calc(var(--base) * 0.5) var(--base) calc(var(--base) * 0.5) var(--icon-width);
background-color: transparent;
}
}
@@ -21,6 +44,32 @@
display: flex;
align-items: center;
gap: calc(var(--base) / 4);
padding: calc(var(--base) * 0.5);
grid-column: 3/4;
}
@include small-break {
min-height: 40px;
background-color: transparent;
&:has(.search-bar__actions) {
row-gap: calc(var(--base) / 2);
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
}
.search-filter {
background-color: var(--search-bg);
}
&__actions {
grid-row: 2/3;
grid-column: 1/3;
display: flex;
align-items: center;
gap: calc(var(--base) / 4);
padding: 0;
}
}
}
}

View File

@@ -7,16 +7,14 @@ const baseClass = 'search-bar'
type SearchBarProps = {
Actions?: React.ReactNode[]
className?: string
filterKey?: string
label?: string
onSearchChange?: (search: string) => void
onSearchChange: (search: string) => void
searchQueryParam?: string
}
export function SearchBar({
Actions,
className,
filterKey,
label,
label = 'Search...',
onSearchChange,
searchQueryParam,
}: SearchBarProps) {
@@ -25,7 +23,6 @@ export function SearchBar({
<SearchIcon />
<SearchFilter
handleChange={onSearchChange}
key={filterKey || 'search'}
label={label}
searchQueryParam={searchQueryParam}
/>

View File

@@ -1,51 +1,17 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export type SearchFilterProps = {
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
fieldName?: string
handleChange?: (search: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* Prefer passing in `searchString` instead.
*
* @deprecated
*/
initialParams?: ParsedQs
label: string
searchQueryParam?: string
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
setValue?: (arg: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
value?: string
}
import type { ParsedQs } from 'qs-esm'
import { usePathname } from 'next/navigation.js'
import type { SearchFilterProps } from './types.js'
import { useDebounce } from '../../hooks/useDebounce.js'
import './index.scss'
const baseClass = 'search-filter'
export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
export function SearchFilter(props: SearchFilterProps) {
const { handleChange, initialParams, label, searchQueryParam } = props
const searchParam = initialParams?.search || searchQueryParam
const pathname = usePathname()
const [search, setSearch] = useState(typeof searchParam === 'string' ? searchParam : undefined)
/**
@@ -67,7 +33,12 @@ export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
setSearch(searchParam as string)
previousSearch.current = searchParam as string
}
}, [searchParam, pathname])
return () => {
shouldUpdateState.current = true
previousSearch.current = undefined
}
}, [searchParam])
useEffect(() => {
if (debouncedSearch !== previousSearch.current && shouldUpdateState.current) {

View File

@@ -0,0 +1,33 @@
import type { ParsedQs } from 'qs-esm'
export type SearchFilterProps = {
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
fieldName?: string
handleChange?: (search: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* Prefer passing in `searchString` instead.
*
* @deprecated
*/
initialParams?: ParsedQs
label: string
searchQueryParam?: string
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
setValue?: (arg: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
value?: string
}

View File

@@ -24,6 +24,7 @@ export const Status: React.FC = () => {
hasPublishedDoc,
incrementVersionCount,
isTrashed,
savedDocumentData: doc,
setHasPublishedDoc,
setMostRecentVersionIsAutosaved,
setUnpublishedVersionCount,
@@ -37,6 +38,7 @@ export const Status: React.FC = () => {
routes: { api },
serverURL,
},
getEntityConfig,
} = useConfig()
const { reset: resetForm } = useForm()
@@ -46,16 +48,22 @@ export const Status: React.FC = () => {
const unPublishModalSlug = `confirm-un-publish-${id}`
const revertModalSlug = `confirm-revert-${id}`
let statusToRender: 'changed' | 'draft' | 'published'
let statusToRender: 'changed' | 'draft' | 'published' = 'draft'
if (unpublishedVersionCount > 0 && hasPublishedDoc) {
statusToRender = 'changed'
} else if (!hasPublishedDoc) {
statusToRender = 'draft'
} else if (hasPublishedDoc && unpublishedVersionCount <= 0) {
statusToRender = 'published'
const collectionConfig = getEntityConfig({ collectionSlug })
const globalConfig = getEntityConfig({ globalSlug })
const docConfig = collectionConfig || globalConfig
const autosaveEnabled =
typeof docConfig?.versions?.drafts === 'object' ? docConfig.versions.drafts.autosave : false
if (autosaveEnabled) {
if (hasPublishedDoc) {
statusToRender = unpublishedVersionCount > 0 ? 'changed' : 'published'
}
} else {
statusToRender = doc._status || 'draft'
}
const displayStatusKey = isTrashed
? hasPublishedDoc
? 'previouslyPublished'
@@ -190,7 +198,8 @@ export const Status: React.FC = () => {
/>
</React.Fragment>
)}
{!isTrashed && canUpdate && statusToRender === 'changed' && (
{((!isTrashed && canUpdate && statusToRender === 'changed') ||
statusToRender === 'draft') && (
<React.Fragment>
&nbsp;&mdash;&nbsp;
<Button

View File

@@ -407,3 +407,4 @@ export { parseSearchParams } from '../../utilities/parseSearchParams.js'
export { FieldDiffLabel } from '../../elements/FieldDiffLabel/index.js'
export { FieldDiffContainer } from '../../elements/FieldDiffContainer/index.js'
export { formatTimeToNow } from '../../utilities/formatDocTitle/formatDateTitle.js'
export { isFieldRTL } from '../../fields/shared/index.js'

View File

@@ -15,7 +15,7 @@ export function isFieldRTL({
localizationConfig,
}: {
fieldLocalized: boolean
fieldRTL: boolean
fieldRTL?: boolean
locale: Locale
localizationConfig?: SanitizedLocalizationConfig
}) {

View File

@@ -134,35 +134,37 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
case 'DUPLICATE_ROW': {
const { path, rowIndex } = action
const { remainingFields, rows } = separateRows(path, state)
const rowsMetadata = [...(state[path].rows || [])]
const rowsWithDuplicate = [...(state[path].rows || [])]
const duplicateRowMetadata = deepCopyObjectSimpleWithoutReactComponents(
rowsMetadata[rowIndex],
)
const newRow = deepCopyObjectSimpleWithoutReactComponents(rowsWithDuplicate[rowIndex])
if (duplicateRowMetadata.id) {
duplicateRowMetadata.id = new ObjectId().toHexString()
const newRowID = new ObjectId().toHexString()
if (newRow.id) {
newRow.id = newRowID
}
if (rowsMetadata[rowIndex]?.customComponents?.RowLabel) {
duplicateRowMetadata.customComponents = {
RowLabel: rowsMetadata[rowIndex].customComponents.RowLabel,
if (rowsWithDuplicate[rowIndex]?.customComponents?.RowLabel) {
newRow.customComponents = {
RowLabel: rowsWithDuplicate[rowIndex].customComponents.RowLabel,
}
}
const duplicateRowState = deepCopyObjectSimpleWithoutReactComponents(rows[rowIndex])
if (duplicateRowState.id) {
duplicateRowState.id.value = new ObjectId().toHexString()
duplicateRowState.id.initialValue = new ObjectId().toHexString()
duplicateRowState.id.value = newRowID
duplicateRowState.id.initialValue = newRowID
}
for (const key of Object.keys(duplicateRowState).filter((key) => key.endsWith('.id'))) {
const idState = duplicateRowState[key]
const newNestedFieldID = new ObjectId().toHexString()
if (idState && typeof idState.value === 'string' && ObjectId.isValid(idState.value)) {
duplicateRowState[key].value = new ObjectId().toHexString()
duplicateRowState[key].initialValue = new ObjectId().toHexString()
duplicateRowState[key].value = newNestedFieldID
duplicateRowState[key].initialValue = newNestedFieldID
}
}
@@ -170,7 +172,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
if (Object.keys(duplicateRowState).length > 0) {
// Add new object containing subfield names to unflattenedRows array
rows.splice(rowIndex + 1, 0, duplicateRowState)
rowsMetadata.splice(rowIndex + 1, 0, duplicateRowMetadata)
rowsWithDuplicate.splice(rowIndex + 1, 0, newRow)
}
const newState = {
@@ -179,7 +181,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
[path]: {
...state[path],
disableFormData: true,
rows: rowsMetadata,
rows: rowsWithDuplicate,
value: rows.length,
},
}

View File

@@ -27,7 +27,9 @@ export const RowLabelProvider: React.FC<Props<unknown>> = ({ children, path, row
const data = arrayData || collapsibleData
return <RowLabel value={{ data, path, rowNumber }}>{children}</RowLabel>
const contextValue = React.useMemo(() => ({ data, path, rowNumber }), [data, path, rowNumber])
return <RowLabel value={contextValue}>{children}</RowLabel>
}
export const useRowLabel = <T,>() => {

View File

@@ -9,7 +9,6 @@ import React, { createContext, use, useCallback, useEffect, useState } from 'rea
import { toast } from 'sonner'
import { stayLoggedInModalSlug } from '../../elements/StayLoggedIn/index.js'
import { useDebounce } from '../../hooks/useDebounce.js'
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js'
@@ -17,6 +16,7 @@ import { useConfig } from '../Config/index.js'
import { useRouteTransition } from '../RouteTransition/index.js'
export type UserWithToken<T = ClientUser> = {
/** seconds until expiration */
exp: number
token: string
user: T
@@ -33,13 +33,13 @@ export type AuthContext<T = ClientUser> = {
setUser: (user: null | UserWithToken<T>) => void
strategy?: string
token?: string
tokenExpiration?: number
tokenExpirationMs?: number
user?: null | T
}
const Context = createContext({} as AuthContext)
const maxTimeoutTime = 2147483647
const maxTimeoutMs = 2147483647
type Props = {
children: React.ReactNode
@@ -52,9 +52,6 @@ export function AuthProvider({
permissions: initialPermissions,
user: initialUser,
}: Props) {
const [user, setUserInMemory] = useState<ClientUser | null>(initialUser)
const [tokenInMemory, setTokenInMemory] = useState<string>()
const [tokenExpiration, setTokenExpiration] = useState<number>()
const pathname = usePathname()
const router = useRouter()
@@ -62,6 +59,7 @@ export function AuthProvider({
const {
admin: {
autoRefresh,
routes: { inactivity: logoutInactivityRoute },
user: userSlug,
},
@@ -69,15 +67,21 @@ export function AuthProvider({
serverURL,
} = config
const [permissions, setPermissions] = useState<SanitizedPermissions>(initialPermissions)
const { i18n } = useTranslation()
const { closeAllModals, openModal } = useModal()
const [lastLocationChange, setLastLocationChange] = useState(0)
const debouncedLocationChange = useDebounce(lastLocationChange, 10000)
const refreshTokenTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>(null)
const { startRouteTransition } = useRouteTransition()
const [user, setUserInMemory] = useState<ClientUser | null>(initialUser)
const [tokenInMemory, setTokenInMemory] = useState<string>()
const [tokenExpirationMs, setTokenExpirationMs] = useState<number>()
const [permissions, setPermissions] = useState<SanitizedPermissions>(initialPermissions)
const [forceLogoutBufferMs, setForceLogoutBufferMs] = useState<number>(120_000)
const [fetchedUserOnMount, setFetchedUserOnMount] = useState(false)
const refreshTokenTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>(null)
const reminderTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>(null)
const forceLogOutTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>(null)
const id = user?.id
const redirectToInactivityRoute = useCallback(() => {
@@ -94,61 +98,86 @@ export function AuthProvider({
}, [router, adminRoute, logoutInactivityRoute, closeAllModals, startRouteTransition])
const revokeTokenAndExpire = useCallback(() => {
setUserInMemory(null)
setTokenInMemory(undefined)
setTokenExpiration(undefined)
setTokenExpirationMs(undefined)
clearTimeout(refreshTokenTimeoutRef.current)
}, [])
const setNewUser = useCallback(
(userResponse: null | UserWithToken) => {
clearTimeout(reminderTimeoutRef.current)
clearTimeout(forceLogOutTimeoutRef.current)
if (userResponse?.user) {
setUserInMemory(userResponse.user)
setTokenInMemory(userResponse.token)
setTokenExpiration(userResponse.exp)
setTokenExpirationMs(userResponse.exp * 1000)
const expiresInMs = Math.max(
0,
Math.min((userResponse.exp ?? 0) * 1000 - Date.now(), maxTimeoutMs),
)
if (expiresInMs) {
const nextForceLogoutBufferMs = Math.min(60_000, expiresInMs / 2)
setForceLogoutBufferMs(nextForceLogoutBufferMs)
reminderTimeoutRef.current = setTimeout(
() => {
if (autoRefresh) {
refreshCookieEvent()
} else {
openModal(stayLoggedInModalSlug)
}
},
Math.max(expiresInMs - nextForceLogoutBufferMs, 0),
)
forceLogOutTimeoutRef.current = setTimeout(() => {
revokeTokenAndExpire()
redirectToInactivityRoute()
}, expiresInMs)
}
} else {
setUserInMemory(null)
revokeTokenAndExpire()
}
},
[revokeTokenAndExpire],
[autoRefresh, redirectToInactivityRoute, revokeTokenAndExpire, openModal],
)
const refreshCookie = useCallback(
(forceRefresh?: boolean) => {
const now = Math.round(new Date().getTime() / 1000)
const remainingTime = (typeof tokenExpiration === 'number' ? tokenExpiration : 0) - now
if (forceRefresh || (tokenExpiration && remainingTime < 120)) {
refreshTokenTimeoutRef.current = setTimeout(() => {
async function refresh() {
try {
const request = await requests.post(
`${serverURL}${apiRoute}/${userSlug}/refresh-token?refresh`,
{
headers: {
'Accept-Language': i18n.language,
},
},
)
if (request.status === 200) {
const json = await request.json()
setNewUser(json)
} else {
setNewUser(null)
redirectToInactivityRoute()
}
} catch (e) {
toast.error(e.message)
}
}
void refresh()
}, 1000)
if (!id) {
return
}
return () => {
const expiresInMs = Math.max(0, (tokenExpirationMs ?? 0) - Date.now())
if (forceRefresh || (tokenExpirationMs && expiresInMs < forceLogoutBufferMs * 2)) {
clearTimeout(refreshTokenTimeoutRef.current)
refreshTokenTimeoutRef.current = setTimeout(async () => {
try {
const request = await requests.post(
`${serverURL}${apiRoute}/${userSlug}/refresh-token?refresh`,
{
headers: {
'Accept-Language': i18n.language,
},
},
)
if (request.status === 200) {
const json: UserWithToken = await request.json()
setNewUser(json)
} else {
setNewUser(null)
redirectToInactivityRoute()
}
} catch (e) {
toast.error(e.message)
}
}, 1000)
}
},
[
@@ -157,8 +186,10 @@ export function AuthProvider({
redirectToInactivityRoute,
serverURL,
setNewUser,
tokenExpiration,
tokenExpirationMs,
userSlug,
forceLogoutBufferMs,
id,
],
)
@@ -172,7 +203,7 @@ export function AuthProvider({
})
if (request.status === 200) {
const json = await request.json()
const json: UserWithToken = await request.json()
if (!skipSetUser) {
setNewUser(json)
}
@@ -183,11 +214,10 @@ export function AuthProvider({
setNewUser(null)
redirectToInactivityRoute()
}
return null
} catch (e) {
toast.error(`Refreshing token failed: ${e.message}`)
return null
}
return null
},
[apiRoute, i18n.language, redirectToInactivityRoute, serverURL, setNewUser, userSlug, user],
)
@@ -247,10 +277,8 @@ export function AuthProvider({
if (request.status === 200) {
const json: UserWithToken = await request.json()
const user = null
setNewUser(json)
return user
return json?.user || null
}
} catch (e) {
toast.error(`Fetching user failed: ${e.message}`)
@@ -259,62 +287,35 @@ export function AuthProvider({
return null
}, [serverURL, apiRoute, userSlug, i18n.language, setNewUser])
const fetchFullUserEvent = useEffectEvent(fetchFullUser)
// On mount, get user and set
useEffect(() => {
void fetchFullUserEvent()
}, [])
const refreshCookieEvent = useEffectEvent(() => {
if (id) {
refreshCookie()
}
})
// When location changes, refresh cookie
const refreshCookieEvent = useEffectEvent(refreshCookie)
useEffect(() => {
// when location changes, refresh cookie
refreshCookieEvent()
}, [debouncedLocationChange])
useEffect(() => {
setLastLocationChange(Date.now())
}, [pathname])
const fetchFullUserEvent = useEffectEvent(fetchFullUser)
useEffect(() => {
let reminder: ReturnType<typeof setTimeout>
let forceLogOut: ReturnType<typeof setTimeout>
const now = Math.round(new Date().getTime() / 1000)
const remainingTime = typeof tokenExpiration === 'number' ? tokenExpiration - now : 0
const remindInTimeFromNow = Math.max(Math.min((remainingTime - 60) * 1000, maxTimeoutTime), 0)
const forceLogOutInTimeFromNow = Math.max(Math.min(remainingTime * 1000, maxTimeoutTime), 0)
if (!user) {
clearTimeout(reminder)
clearTimeout(forceLogOut)
return
async function fetchUserOnMount() {
await fetchFullUserEvent()
setFetchedUserOnMount(true)
}
if (remainingTime > 0) {
reminder = setTimeout(() => {
openModal(stayLoggedInModalSlug)
}, remindInTimeFromNow)
void fetchUserOnMount()
}, [])
forceLogOut = setTimeout(() => {
setNewUser(null)
redirectToInactivityRoute()
}, forceLogOutInTimeFromNow)
}
useEffect(
() => () => {
// remove all timeouts on unmount
clearTimeout(refreshTokenTimeoutRef.current)
clearTimeout(reminderTimeoutRef.current)
clearTimeout(forceLogOutTimeoutRef.current)
},
[],
)
return () => {
if (reminder) {
clearTimeout(reminder)
}
if (forceLogOut) {
clearTimeout(forceLogOut)
}
}
}, [tokenExpiration, openModal, i18n, setNewUser, user, redirectToInactivityRoute])
if (!user && !fetchedUserOnMount) {
return null
}
return (
<Context
@@ -328,6 +329,7 @@ export function AuthProvider({
setPermissions,
setUser: setNewUser,
token: tokenInMemory,
tokenExpirationMs,
user,
}}
>

View File

@@ -1,6 +1,5 @@
'use client'
import type { LivePreviewConfig } from 'payload'
import type { fieldSchemaToJSON } from 'payload/shared'
import type { Dispatch } from 'react'
import type React from 'react'
@@ -13,7 +12,6 @@ export interface LivePreviewContextType {
appIsReady: boolean
breakpoint: LivePreviewConfig['breakpoints'][number]['name']
breakpoints: LivePreviewConfig['breakpoints']
fieldSchemaJSON?: ReturnType<typeof fieldSchemaToJSON>
iframeHasLoaded: boolean
iframeRef: React.RefObject<HTMLIFrameElement | null>
isLivePreviewEnabled: boolean
@@ -54,7 +52,6 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
appIsReady: false,
breakpoint: undefined,
breakpoints: undefined,
fieldSchemaJSON: undefined,
iframeHasLoaded: false,
iframeRef: undefined,
isLivePreviewEnabled: undefined,

View File

@@ -2,13 +2,11 @@
import type { CollectionPreferences, LivePreviewConfig } from 'payload'
import { DndContext } from '@dnd-kit/core'
import { fieldSchemaToJSON } from 'payload/shared'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { usePopupWindow } from '../../hooks/usePopupWindow.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { usePreferences } from '../../providers/Preferences/index.js'
import { useConfig } from '../Config/index.js'
import { customCollisionDetection } from './collisionDetection.js'
import { LivePreviewContext } from './context.js'
import { sizeReducer } from './sizeReducer.js'
@@ -90,8 +88,6 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
const [iframeHasLoaded, setIframeHasLoaded] = useState(false)
const { config, getEntityConfig } = useConfig()
const [zoom, setZoom] = useState(1)
const [position, setPosition] = useState({ x: 0, y: 0 })
@@ -103,13 +99,9 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
width: 0,
})
const entityConfig = getEntityConfig({ collectionSlug, globalSlug })
const [breakpoint, setBreakpoint] =
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
const [fieldSchemaJSON] = useState(() => fieldSchemaToJSON(entityConfig?.fields || [], config))
// The toolbar needs to freely drag and drop around the page
const handleDragEnd = (ev) => {
// only update position if the toolbar is completely within the preview area
@@ -232,7 +224,6 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
appIsReady,
breakpoint,
breakpoints,
fieldSchemaJSON,
iframeHasLoaded,
iframeRef,
isLivePreviewEnabled,

View File

@@ -204,6 +204,12 @@ export const buildFormState = async (
? fieldOrEntityConfig.fields
: [fieldOrEntityConfig]
// Ensure data.id is present during form state requests, where the data
// is passed from the client as an argument, without the ID
if (!data.id && id) {
data.id = id
}
const formStateResult = await fieldSchemasToFormState({
id,
clientFieldSchemaMap: clientSchemaMap,

View File

@@ -30,7 +30,27 @@ export const reduceFieldsToOptions = ({
}: ReduceFieldOptionsArgs): ReducedField[] => {
return fields.reduce((reduced, field) => {
// Do not filter out `field.admin.disableListFilter` fields here, as these should still render as disabled if they appear in the URL query
if (fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) {
// Filter out `virtual: true` fields since they are regular virtuals and not backed by a DB field
if (
(fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) ||
('virtual' in field && field.virtual === true)
) {
return reduced
}
// Handle virtual:string fields (virtual relationships, e.g. "post.title")
if ('virtual' in field && typeof field.virtual === 'string') {
const baseLabel = ('label' in field && field.label) || ('name' in field && field.name) || ''
const localizedLabel = getTranslation(baseLabel, i18n)
reduced.push({
label: localizedLabel,
plainTextLabel: localizedLabel,
value: field.virtual, // e.g. "post.title"
...fieldTypes[field.type],
field,
operators: fieldTypes[field.type].operators,
})
return reduced
}

387
pnpm-lock.yaml generated
View File

@@ -1247,35 +1247,35 @@ importers:
specifier: 2.0.0
version: 2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@lexical/headless':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/html':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/link':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/list':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/mark':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/react':
specifier: 0.28.0
version: 0.28.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.20)
specifier: 0.35.0
version: 0.35.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.20)
'@lexical/rich-text':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/selection':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/table':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@lexical/utils':
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
'@payloadcms/next':
specifier: workspace:*
version: link:../next
@@ -1307,8 +1307,8 @@ importers:
specifier: 1.2.121
version: 1.2.121
lexical:
specifier: 0.28.0
version: 0.28.0
specifier: 0.35.0
version: 0.35.0
mdast-util-from-markdown:
specifier: 2.0.2
version: 2.0.2
@@ -1353,8 +1353,8 @@ importers:
specifier: 7.27.1
version: 7.27.1(@babel/core@7.27.3)
'@lexical/eslint-plugin':
specifier: 0.28.0
version: 0.28.0(eslint@9.22.0(jiti@1.21.6))
specifier: 0.35.0
version: 0.35.0(eslint@9.22.0(jiti@1.21.6))
'@payloadcms/eslint-config':
specifier: workspace:*
version: link:../eslint-config
@@ -3934,21 +3934,42 @@ packages:
'@floating-ui/core@1.6.8':
resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
'@floating-ui/dom@1.6.12':
resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==}
'@floating-ui/dom@1.7.4':
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
'@floating-ui/react-dom@2.1.2':
resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
peerDependencies:
react: 19.1.0
react-dom: 19.1.0
'@floating-ui/react-dom@2.1.6':
resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==}
peerDependencies:
react: 19.1.0
react-dom: 19.1.0
'@floating-ui/react@0.27.16':
resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==}
peerDependencies:
react: 19.1.0
react-dom: 19.1.0
'@floating-ui/react@0.27.2':
resolution: {integrity: sha512-k/yP6a9K9QwhLfIu87iUZxCH6XN5z5j/VUHHq0dEnbZYY2Y9jz68E/LXFtK8dkiaYltS2WYohnyKC0VcwVneVg==}
peerDependencies:
react: 19.1.0
react-dom: 19.1.0
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
@@ -4454,82 +4475,82 @@ packages:
'@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
'@lexical/clipboard@0.28.0':
resolution: {integrity: sha512-LYqion+kAwFQJStA37JAEMxTL/m1WlZbotDfM/2WuONmlO0yWxiyRDI18oeCwhBD6LQQd9c3Ccxp9HFwUG1AVw==}
'@lexical/clipboard@0.35.0':
resolution: {integrity: sha512-ko7xSIIiayvDiqjNDX6fgH9RlcM6r9vrrvJYTcfGVBor5httx16lhIi0QJZ4+RNPvGtTjyFv4bwRmsixRRwImg==}
'@lexical/code@0.28.0':
resolution: {integrity: sha512-9LOKSWdRhxqAKRq5yveNC21XKtW4h2rmFNTucwMWZ9vLu9xteOHEwZdO1Qv82PFUmgCpAhg6EntmnZu9xD3K7Q==}
'@lexical/code@0.35.0':
resolution: {integrity: sha512-ox4DZwETQ9IA7+DS6PN8RJNwSAF7RMjL7YTVODIqFZ5tUFIf+5xoCHbz7Fll0Bvixlp12hVH90xnLwTLRGpkKw==}
'@lexical/devtools-core@0.28.0':
resolution: {integrity: sha512-Fk4itAjZ+MqTYXN84aE5RDf+wQX67N5nyo3JVxQTFZGAghx7Ux1xLWHB25zzD0YfjMtJ0NQROAbE3xdecZzxcQ==}
'@lexical/devtools-core@0.35.0':
resolution: {integrity: sha512-C2wwtsMCR6ZTfO0TqpSM17RLJWyfHmifAfCTjFtOJu15p3M6NO/nHYK5Mt7YMQteuS89mOjB4ng8iwoLEZ6QpQ==}
peerDependencies:
react: 19.1.0
react-dom: 19.1.0
'@lexical/dragon@0.28.0':
resolution: {integrity: sha512-T6T8YaHnhU863ruuqmRHTLUYa8sfg/ArYcrnNGZGfpvvFTfFjpWb/ELOvOWo8N6Y/4fnSLjQ20aXexVW1KcTBQ==}
'@lexical/dragon@0.35.0':
resolution: {integrity: sha512-SL6mT5pcqrt6hEbJ16vWxip5+r3uvMd0bQV5UUxuk+cxIeuP86iTgRh0HFR7SM2dRTYovL6/tM/O+8QLAUGTIg==}
'@lexical/eslint-plugin@0.28.0':
resolution: {integrity: sha512-aIerq82L8A/y8be7zTPF5ZYqOtzEbCRl+kVxoc8NmwJxFPoVJkdS3rblmKu2WYlJZbR1VQbpcVhQ+K14ynMViQ==}
'@lexical/eslint-plugin@0.35.0':
resolution: {integrity: sha512-YR9YF5BhCfIwqRmeC80XXnYck6RUkI6ZEubWbv1jdwrrEpw/BMIHBxHDSlUDE5kZNLsOWma5ch+rvAZvUGYv/w==}
peerDependencies:
eslint: '>=7.31.0 || ^8.0.0'
'@lexical/hashtag@0.28.0':
resolution: {integrity: sha512-zcqX9Qna4lj96bAUfwSQSVEhYQ0O5erSjrIhOVqEgeQ5ubz0EvqnnMbbwNHIb2n6jzSwAvpD/3UZJZtolh+zVg==}
'@lexical/hashtag@0.35.0':
resolution: {integrity: sha512-LYJWzXuO2ZjKsvQwrLkNZiS2TsjwYkKjlDgtugzejquTBQ/o/nfSn/MmVx6EkYLOYizaJemmZbz3IBh+u732FA==}
'@lexical/headless@0.28.0':
resolution: {integrity: sha512-btcaTfw9I/xQ/XYom6iKWgsPecmRawGd/5jOhP7QDtLUp7gxgM7/kiCZFYa8jDJO6j20rXuWTkc81ynVpKvjow==}
'@lexical/headless@0.35.0':
resolution: {integrity: sha512-UPmCqOsdGGC7/8Fkae2ADkTQfxTZOKxNEVKuqPfCkFs4Bag3s4z3V61jE+wYzqyU8eJh4DqZYSHoPzZCj8P9jg==}
'@lexical/history@0.28.0':
resolution: {integrity: sha512-CHzDxaGDn6qCFFhU0YKP1B8sgEb++0Ksqsj6BfDL/6TMxoLNQwRQhP3BUNNXl1kvUhxTQZgk3b9MjJZRaFKG9Q==}
'@lexical/history@0.35.0':
resolution: {integrity: sha512-onjDRLLxGbCfHexSxxrQaDaieIHyV28zCDrbxR5dxTfW8F8PxjuNyuaG0z6o468AXYECmclxkP+P4aT6poHEpQ==}
'@lexical/html@0.28.0':
resolution: {integrity: sha512-ayb0FPxr55Ko99/d9ewbfrApul4L0z+KpU2ZG03im7EvUPVLyIGLx4S0QguMDvQh0Vu+eJ7/EESuonDs5BCe3A==}
'@lexical/html@0.35.0':
resolution: {integrity: sha512-rXGFE5S5rKsg3tVnr1s4iEgOfCApNXGpIFI3T2jGEShaCZ5HLaBY9NVBXnE9Nb49e9bkDkpZ8FZd1qokCbQXbw==}
'@lexical/link@0.28.0':
resolution: {integrity: sha512-T5VKxpOnML5DcXv2lW3Le0vjNlcbdohZjS9f6PAvm6eX8EzBKDpLQCopr1/0KGdlLd1QrzQsykQrdU7ieC4LRg==}
'@lexical/link@0.35.0':
resolution: {integrity: sha512-+0Wx6cBwO8TfdMzpkYFacsmgFh8X1rkiYbq3xoLvk3qV8upYxaMzK1s8Q1cpKmWyI0aZrU6z7fiK4vUqB7+69w==}
'@lexical/list@0.28.0':
resolution: {integrity: sha512-3a8QcZ75n2TLxP+xkSPJ2V15jsysMLMe0YoObG+ew/sioVelIU8GciYsWBo5GgQmwSzJNQJeK5cJ9p1b71z2cg==}
'@lexical/list@0.35.0':
resolution: {integrity: sha512-owsmc8iwgExBX8sFe8fKTiwJVhYULt9hD1RZ/HwfaiEtRZZkINijqReOBnW2mJfRxBzhFSWc4NG3ISB+fHYzqw==}
'@lexical/mark@0.28.0':
resolution: {integrity: sha512-v5PzmTACsJrw3GvNZy2rgPxrNn9InLvLFoKqrSlNhhyvYNIAcuC4KVy00LKLja43Gw/fuB3QwKohYfAtM3yR3g==}
'@lexical/mark@0.35.0':
resolution: {integrity: sha512-W0hwMTAVeexvpk9/+J6n1G/sNkpI/Meq1yeDazahFLLAwXLHtvhIAq2P/klgFknDy1hr8X7rcsQuN/bqKcKHYg==}
'@lexical/markdown@0.28.0':
resolution: {integrity: sha512-F3JXClqN4cjmXYLDK0IztxkbZuqkqS/AVbxnhGvnDYHQ9Gp8l7BonczhOiPwmJCDubJrAACP0L9LCqyt0jDRFw==}
'@lexical/markdown@0.35.0':
resolution: {integrity: sha512-BlNyXZAt4gWidMw0SRWrhBETY1BpPglFBZI7yzfqukFqgXRh7HUQA28OYeI/nsx9pgNob8TiUduUwShqqvOdEA==}
'@lexical/offset@0.28.0':
resolution: {integrity: sha512-/SMDQgBPeWM936t04mtH6UAn3xAjP/meu9q136bcT3S7p7V8ew9JfNp9aznTPTx+2W3brJORAvUow7Xn1fSHmw==}
'@lexical/offset@0.35.0':
resolution: {integrity: sha512-DRE4Df6qYf2XiV6foh6KpGNmGAv2ANqt3oVXpyS6W8hTx3+cUuAA1APhCZmLNuU107um4zmHym7taCu6uXW5Yg==}
'@lexical/overflow@0.28.0':
resolution: {integrity: sha512-ppmhHXEZVicBm05w9EVflzwFavTVNAe4q0bkabWUeW0IoCT3Vg2A3JT7PC9ypmp+mboUD195foFEr1BBSv1Y8Q==}
'@lexical/overflow@0.35.0':
resolution: {integrity: sha512-B25YvnJQTGlZcrNv7b0PJBLWq3tl8sql497OHfYYLem7EOMPKKDGJScJAKM/91D4H/mMAsx5gnA/XgKobriuTg==}
'@lexical/plain-text@0.28.0':
resolution: {integrity: sha512-Jj2dCMDEfRuVetfDKcUes8J5jvAfZrLnILFlHxnu7y+lC+7R/NR403DYb3NJ8H7+lNiH1K15+U2K7ewbjxS6KQ==}
'@lexical/plain-text@0.35.0':
resolution: {integrity: sha512-lwBCUNMJf7Gujp2syVWMpKRahfbTv5Wq+H3HK1Q1gKH1P2IytPRxssCHvexw9iGwprSyghkKBlbF3fGpEdIJvQ==}
'@lexical/react@0.28.0':
resolution: {integrity: sha512-dWPnxrKrbQFjNqExqnaAsV0UEUgw/5M1ZYRWd5FGBGjHqVTCaX2jNHlKLMA68Od0VPIoOX2Zy1TYZ8ZKtsj5Dg==}
'@lexical/react@0.35.0':
resolution: {integrity: sha512-uYAZSqumH8tRymMef+A0f2hQvMwplKK9DXamcefnk3vSNDHHqRWQXpiUo6kD+rKWuQmMbVa5RW4xRQebXEW+1A==}
peerDependencies:
react: 19.1.0
react-dom: 19.1.0
'@lexical/rich-text@0.28.0':
resolution: {integrity: sha512-y+vUWI+9uFupIb9UvssKU/DKcT9dFUZuQBu7utFkLadxCNyXQHeRjxzjzmvFiM3DBV0guPUDGu5VS5TPnIA+OA==}
'@lexical/rich-text@0.35.0':
resolution: {integrity: sha512-qEHu8g7vOEzz9GUz1VIUxZBndZRJPh9iJUFI+qTDHj+tQqnd5LCs+G9yz6jgNfiuWWpezTp0i1Vz/udNEuDPKQ==}
'@lexical/selection@0.28.0':
resolution: {integrity: sha512-AJDi67Nsexyejzp4dEQSVoPov4P+FJ0t1v6DxUU+YmcvV56QyJQi6ue0i/xd8unr75ZufzLsAC0cDJJCEI7QDA==}
'@lexical/selection@0.35.0':
resolution: {integrity: sha512-mMtDE7Q0nycXdFTTH/+ta6EBrBwxBB4Tg8QwsGntzQ1Cq//d838dpXpFjJOqHEeVHUqXpiuj+cBG8+bvz/rPRw==}
'@lexical/table@0.28.0':
resolution: {integrity: sha512-HMPCwXdj0sRWdlDzsHcNWRgbeKbEhn3L8LPhFnTq7q61gZ4YW2umdmuvQFKnIBcKq49drTH8cUwZoIwI8+AEEw==}
'@lexical/table@0.35.0':
resolution: {integrity: sha512-9jlTlkVideBKwsEnEkqkdg7A3mije1SvmfiqoYnkl1kKJCLA5iH90ywx327PU0p+bdnURAytWUeZPXaEuEl2OA==}
'@lexical/text@0.28.0':
resolution: {integrity: sha512-PT/A2RZv+ktn7SG/tJkOpGlYE6zjOND59VtRHnV/xciZ+jEJVaqAHtWjhbWibAIZQAkv/O7UouuDqzDaNTSGAA==}
'@lexical/text@0.35.0':
resolution: {integrity: sha512-uaMh46BkysV8hK8wQwp5g/ByZW+2hPDt8ahAErxtf8NuzQem1FHG/f5RTchmFqqUDVHO3qLNTv4AehEGmXv8MA==}
'@lexical/utils@0.28.0':
resolution: {integrity: sha512-Qw00DjkS1nRK7DLSgqJpJ77Ti2AuiOQ6m5eM38YojoWXkVmoxqKAUMaIbVNVKqjFgrQvKFF46sXxIJPbUQkB0w==}
'@lexical/utils@0.35.0':
resolution: {integrity: sha512-2H393EYDnFznYCDFOW3MHiRzwEO5M/UBhtUjvTT+9kc+qhX4U3zc8ixQalo5UmZ5B2nh7L/inXdTFzvSRXtsRA==}
'@lexical/yjs@0.28.0':
resolution: {integrity: sha512-rKHpUEd3nrvMY7ghmOC0AeGSYT7YIviba+JViaOzrCX4/Wtv5C/3Sl7Io12Z9k+s1BKmy7C28bOdQHvRWaD7vQ==}
'@lexical/yjs@0.35.0':
resolution: {integrity: sha512-3DSP7QpmTGYU9bN/yljP0PIao4tNIQtsR4ycauWNSawxs/GQCZtSmAPcLRnCm6qpqsDDjUtKjO/1Ej8FRp0m0w==}
peerDependencies:
yjs: '>=13.5.22'
@@ -9540,8 +9561,8 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lexical@0.28.0:
resolution: {integrity: sha512-dLE3O1PZg0TlZxRQo9YDpjCjDUj8zluGyBO9MHdjo21qZmMUNrxQPeCRt8fn2s5l4HKYFQ1YNgl7k1pOJB/vZQ==}
lexical@0.35.0:
resolution: {integrity: sha512-3VuV8xXhh5xJA6tzvfDvE0YBCMkIZUmxtRilJQDDdCgJCc+eut6qAv2qbN+pbqvarqcQqPN1UF+8YvsjmyOZpw==}
lib0@0.2.98:
resolution: {integrity: sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==}
@@ -14823,17 +14844,40 @@ snapshots:
dependencies:
'@floating-ui/utils': 0.2.8
'@floating-ui/core@1.7.3':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.6.12':
dependencies:
'@floating-ui/core': 1.6.8
'@floating-ui/utils': 0.2.8
'@floating-ui/dom@1.7.4':
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
'@floating-ui/react-dom@2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@floating-ui/dom': 1.6.12
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@floating-ui/react-dom@2.1.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@floating-ui/dom': 1.7.4
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@floating-ui/react@0.27.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@floating-ui/react-dom': 2.1.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@floating-ui/utils': 0.2.10
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
tabbable: 6.2.0
'@floating-ui/react@0.27.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -14842,6 +14886,8 @@ snapshots:
react-dom: 19.1.0(react@19.1.0)
tabbable: 6.2.0
'@floating-ui/utils@0.2.10': {}
'@floating-ui/utils@0.2.8': {}
'@google-cloud/paginator@5.0.2':
@@ -15350,157 +15396,158 @@ snapshots:
'@juggle/resize-observer@3.4.0': {}
'@lexical/clipboard@0.28.0':
'@lexical/clipboard@0.35.0':
dependencies:
'@lexical/html': 0.28.0
'@lexical/list': 0.28.0
'@lexical/selection': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/html': 0.35.0
'@lexical/list': 0.35.0
'@lexical/selection': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/code@0.28.0':
'@lexical/code@0.35.0':
dependencies:
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
prismjs: 1.30.0
'@lexical/devtools-core@0.28.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@lexical/devtools-core@0.35.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@lexical/html': 0.28.0
'@lexical/link': 0.28.0
'@lexical/mark': 0.28.0
'@lexical/table': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/html': 0.35.0
'@lexical/link': 0.35.0
'@lexical/mark': 0.35.0
'@lexical/table': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@lexical/dragon@0.28.0':
'@lexical/dragon@0.35.0':
dependencies:
lexical: 0.28.0
lexical: 0.35.0
'@lexical/eslint-plugin@0.28.0(eslint@9.22.0(jiti@1.21.6))':
'@lexical/eslint-plugin@0.35.0(eslint@9.22.0(jiti@1.21.6))':
dependencies:
eslint: 9.22.0(jiti@1.21.6)
'@lexical/hashtag@0.28.0':
'@lexical/hashtag@0.35.0':
dependencies:
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/headless@0.28.0':
'@lexical/headless@0.35.0':
dependencies:
lexical: 0.28.0
lexical: 0.35.0
'@lexical/history@0.28.0':
'@lexical/history@0.35.0':
dependencies:
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/html@0.28.0':
'@lexical/html@0.35.0':
dependencies:
'@lexical/selection': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/selection': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/link@0.28.0':
'@lexical/link@0.35.0':
dependencies:
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/list@0.28.0':
'@lexical/list@0.35.0':
dependencies:
'@lexical/selection': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/selection': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/mark@0.28.0':
'@lexical/mark@0.35.0':
dependencies:
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/markdown@0.28.0':
'@lexical/markdown@0.35.0':
dependencies:
'@lexical/code': 0.28.0
'@lexical/link': 0.28.0
'@lexical/list': 0.28.0
'@lexical/rich-text': 0.28.0
'@lexical/text': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/code': 0.35.0
'@lexical/link': 0.35.0
'@lexical/list': 0.35.0
'@lexical/rich-text': 0.35.0
'@lexical/text': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/offset@0.28.0':
'@lexical/offset@0.35.0':
dependencies:
lexical: 0.28.0
lexical: 0.35.0
'@lexical/overflow@0.28.0':
'@lexical/overflow@0.35.0':
dependencies:
lexical: 0.28.0
lexical: 0.35.0
'@lexical/plain-text@0.28.0':
'@lexical/plain-text@0.35.0':
dependencies:
'@lexical/clipboard': 0.28.0
'@lexical/selection': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/clipboard': 0.35.0
'@lexical/selection': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/react@0.28.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.20)':
'@lexical/react@0.35.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.20)':
dependencies:
'@lexical/devtools-core': 0.28.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@lexical/dragon': 0.28.0
'@lexical/hashtag': 0.28.0
'@lexical/history': 0.28.0
'@lexical/link': 0.28.0
'@lexical/list': 0.28.0
'@lexical/mark': 0.28.0
'@lexical/markdown': 0.28.0
'@lexical/overflow': 0.28.0
'@lexical/plain-text': 0.28.0
'@lexical/rich-text': 0.28.0
'@lexical/table': 0.28.0
'@lexical/text': 0.28.0
'@lexical/utils': 0.28.0
'@lexical/yjs': 0.28.0(yjs@13.6.20)
lexical: 0.28.0
'@floating-ui/react': 0.27.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@lexical/devtools-core': 0.35.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@lexical/dragon': 0.35.0
'@lexical/hashtag': 0.35.0
'@lexical/history': 0.35.0
'@lexical/link': 0.35.0
'@lexical/list': 0.35.0
'@lexical/mark': 0.35.0
'@lexical/markdown': 0.35.0
'@lexical/overflow': 0.35.0
'@lexical/plain-text': 0.35.0
'@lexical/rich-text': 0.35.0
'@lexical/table': 0.35.0
'@lexical/text': 0.35.0
'@lexical/utils': 0.35.0
'@lexical/yjs': 0.35.0(yjs@13.6.20)
lexical: 0.35.0
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-error-boundary: 3.1.4(react@19.1.0)
transitivePeerDependencies:
- yjs
'@lexical/rich-text@0.28.0':
'@lexical/rich-text@0.35.0':
dependencies:
'@lexical/clipboard': 0.28.0
'@lexical/selection': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/clipboard': 0.35.0
'@lexical/selection': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/selection@0.28.0':
'@lexical/selection@0.35.0':
dependencies:
lexical: 0.28.0
lexical: 0.35.0
'@lexical/table@0.28.0':
'@lexical/table@0.35.0':
dependencies:
'@lexical/clipboard': 0.28.0
'@lexical/utils': 0.28.0
lexical: 0.28.0
'@lexical/clipboard': 0.35.0
'@lexical/utils': 0.35.0
lexical: 0.35.0
'@lexical/text@0.28.0':
'@lexical/text@0.35.0':
dependencies:
lexical: 0.28.0
lexical: 0.35.0
'@lexical/utils@0.28.0':
'@lexical/utils@0.35.0':
dependencies:
'@lexical/list': 0.28.0
'@lexical/selection': 0.28.0
'@lexical/table': 0.28.0
lexical: 0.28.0
'@lexical/list': 0.35.0
'@lexical/selection': 0.35.0
'@lexical/table': 0.35.0
lexical: 0.35.0
'@lexical/yjs@0.28.0(yjs@13.6.20)':
'@lexical/yjs@0.35.0(yjs@13.6.20)':
dependencies:
'@lexical/offset': 0.28.0
'@lexical/selection': 0.28.0
lexical: 0.28.0
'@lexical/offset': 0.35.0
'@lexical/selection': 0.35.0
lexical: 0.35.0
yjs: 13.6.20
'@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5)':
@@ -21399,7 +21446,7 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lexical@0.28.0: {}
lexical@0.35.0: {}
lib0@0.2.98:
dependencies:

View File

@@ -3,7 +3,7 @@ import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { NextRequest } from "next/server"
import { NextRequest } from 'next/server'
import configPromise from '@payload-config'

View File

@@ -9,6 +9,10 @@ import { getClientSideURL } from '@/utilities/getURL'
export const getMediaUrl = (url: string | null | undefined, cacheTag?: string | null): string => {
if (!url) return ''
if (cacheTag && cacheTag !== '') {
cacheTag = encodeURIComponent(cacheTag)
}
// Check if URL already has http/https protocol
if (url.startsWith('http://') || url.startsWith('https://')) {
return cacheTag ? `${url}?${cacheTag}` : url

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