Compare commits

...

49 Commits

Author SHA1 Message Date
Paul Popus
a20ac95d17 remove value if it's empty 2025-02-05 16:55:24 +00:00
tak-amboss
8e0632bdee add e2e test 2025-02-04 16:48:36 +01:00
tak-amboss
4e399adc46 editableProps can be undefined 2025-02-04 15:42:16 +01:00
tak-amboss
aeb0476716 update types 2025-02-04 15:28:39 +01:00
tak-amboss
842000fcfc make text options editable 2025-02-04 15:18:19 +01:00
tak-amboss
d86ebe3a96 disable dropdown menu 2025-02-04 13:25:18 +01:00
tak-amboss
d67dd4e43b disable ClearIndicator 2025-02-04 13:15:54 +01:00
Tylan Davis
1a68fa14bb docs: correct broken NPM badge images on plugin documentation (#10959)
### What?
Fixes broken NPM badge images/links on plugin documentation pages.

### Why?
They were not properly formatted and did not work.

### How?
Corrects the formatting.

Before: https://payloadcms.com/docs/plugins/nested-docs
After:
https://payloadcms.com/docs/dynamic/plugins/nested-docs?branch=docs/npm-badges
2025-02-03 22:45:10 +00:00
Said Akhrarov
b33749905d test: admin list view custom components (#10956)
### What?
This PR adds tests for custom list view components to the existing suite
in `admin/e2e/list-view/`. Custom components are already tested in the
document-level counterpart, and should be tested here as well.

### Why?
Previously, there were no tests for these list view components.
Refactors, features, or changes that impact the importMap, default list
view, etc., could affect how these components get rendered. It's safer
to have tests in place to catch this as custom list view components, in
general, are used quite often.

### How?
This PR adds 5 simple tests that check for the rendering of the
following list view components:
- `BeforeList`
- `BeforeListTable`
- `UI Field Cell`
- `AfterList`
- `AfterListTable`
2025-02-03 16:18:26 -05:00
Jessica Chowdhury
0f85a6e0cc fix(plugin-search): generates full docURL with basePath from next config (#10910)
Fixes #10878. The Search Plugin displays a link within the search
results collection that points to the underlying document that is
related to that result. The href used, however, was not accounting for
any `basePath` provided to the `next.config.js`, leading to a 404 if
using a custom base path. The fix is to use the `Link` component from
`next/link` instead of an anchor tag directly. This will automatically
inject the the base path into the href before rendering it.

This PR also brings back the `CopyToClipboard` component. This makes it
easy for the user to copy the href instead of navigating to it.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-02-03 15:17:07 -05:00
Jacob Fletcher
177127141e chore(plugin-search): improves types (#10955)
There were a number of areas within the Search Plugin where typings
could have been improved, namely:
- The `customProps` sent to the `ReindexButton`. This now uses the
`satisfies` keyword to ensure type strictness.
- The `collectionLabels` prop sent to the `ReindexButtonClient`
component. This is now standardized behind a new
`ResolvedCollectionLabels` type to closely reflect `CollectionLabels`.
This was also converted from unnecessarily invoking a function to being
a basic object.
- The `locale` type sent through `SyncDocArgs`. This now uses
`Locale['code']` from Payload.
2025-02-03 19:53:04 +00:00
Steve Kuznetsov
0a1cc6adcb templates: use typed functions in website template seed endpoint (#10420)
`JSON.parse(JSON.stringify().replace())` is easy to make mistakes with and since we have TypeScript data objects already for the data we're seeding it's pretty easy to just factor these as functions, making their dependencies explicit.
2025-02-03 12:40:22 -07:00
Jacob Fletcher
4a4e90a170 chore(plugin-search): deprecates apiBasePath from config (#10953)
Continuation of #10632. The `apiBasePath` property in the Search Plugin
config is unnecessary. This plugin reads directly from the Payload
config for this property. Exposing it to the plugin's config was likely
a mistake during sanitization before passing it through to the remaining
files. This property was added to resolve the types, but as result,
exposed it to the config unnecessarily. This PR marks this property with
the deprecated flag to prevent breaking changes.
2025-02-03 19:06:05 +00:00
Alessio Gravili
136c90c725 fix(richtext-lexical): link drawer has no fields if parent document create access control is false (#10954)
Previously, the lexical link drawer did not display any fields if the
`create` permission was false, even though the `update` permission was
true.

The issue was a faulty permission check in `RenderFields` that did not
check the top-level permission operation keys for truthiness. It only
checked if the `permissions` variable itself was `true`, or if the
sub-fields had `create` / `update` permissions set to `true`.
2025-02-03 19:02:40 +00:00
Suphon T.
6353cf8bbe fix(plugin-search): gets api route from useConfig (#10632)
This fixes #10631.

Originally the api basepath for the reindex button is resolved during
plugin initialization. Looks like this happens before payload overrides
the config with the `basePath `from the next config.
I've changed it so that it uses the `useConfig` hook, and manually
tested that it works.

![CleanShot 2568-01-17 at 16 03
16@2x](https://github.com/user-attachments/assets/c931577b-2717-4635-b5c6-17aa1b4eb734)
2025-02-03 18:14:21 +00:00
Alessio Gravili
109de8cdb3 chore(deps): bump packages used to build payload (#10950)
Bumps all babel/esbuild/swc/react compiler packages
2025-02-03 16:53:42 +00:00
Alexander Cato
8ace0cab33 docs: correct grammar and improve clarity on preventing-abuse.mdx (#10937)
## What
Refactored the explanation of complexity limits in the
⁠preventing-abuse.mdx documentation to correct grammar and improve
clarity.

## Why
- Grammar fix: The original sentence omitted the preposition "to" ("way
specify" → "way to specify").
- Readability: The long, compound sentence was difficult to parse at a
glance.
- Concept separation: Merging two ideas (defining limits and explaining
scoring) confused the workflow.

## How
- Added the missing "to" to ensure grammatical correctness.
- Split the sentence into two parts:
  1. Introduces the purpose of complexity limits.
  2. Explains how complexity scores enforce these limits.
- Preserved technical accuracy while simplifying the flow.
2025-02-02 19:07:52 -07:00
Marwin Hormiz
58666fbdef examples: added missing sharp dependency to the remix website package (#10931)
When the sharp module is not added to the website package, you get a
reference error when trying to start a production build. This is solved
by just installing the sharp module.

Solves #10929

Co-authored-by: Marwin Hormiz <marwinhormiz@duobit.se>
2025-02-02 21:46:55 +02:00
Jacob Fletcher
2f787a9126 chore(deps): bumps @faceless-ui/window-info to v3.0.1 and @faceless-ui/scroll-info to 2.0.0 (#10913)
Bumps `@faceless-ui/window-info` to v3.0.1` and
`@faceless-ui/scroll-info` to v2.0.0. This gets them both off beta
versions and includes React 19 stable in their peer deps.

The `@faceless-ui/modal` package, however, has yet to be bumped. This
package is waiting on https://github.com/faceless-ui/modal/issues/63 to
be resolved in order to fully deprecate
[`body-scroll-lock`](https://github.com/willmcpo/body-scroll-lock)
before bumping to stable.
2025-01-31 17:39:04 +00:00
Sasha
68a7de2610 fix(db-postgres): select hasMany inside arrays and blocks with versions (#10829)
Fixes https://github.com/payloadcms/payload/issues/10780

Previously, with enabled versions, nested select `hasMany: true` fields
weren't working with SQL database adapters. This was due to wrongly
passed `parent` to select rows data because we store arrays and blocks
in versions a bit differently, using both, `id` and `_uuid` (which
contains the normal Object ID) columns. And unlike with non versions
`_uuid` column isn't actually applicable here as it's not unique, thus
we need to save blocks/arrays first and then map their ObjectIDs to
generated by the database IDs and use them for select fields `parent`
data
2025-01-31 18:26:04 +02:00
Sasha
e1dcb9594c fix(db-postgres): write operations on polymorphic joined collections throw error (#10854)
Fixes https://github.com/payloadcms/payload/issues/10845
See
https://github.com/payloadcms/payload/issues/10845#issuecomment-2620201486
2025-01-31 18:25:47 +02:00
Franco D'Agostino
2043b4a6ea templates: add @ts-ignore in seed to allow initial build on vercel (#10889)
### What?
Add @ts-ignore in seed to allow initial build on vercel

### Why?
The 1-click setup for the vercel-website template doesn't work because
the initial build fails on vercel

### How?
Added some ts-ignore, similarly to the main payload repo
2025-01-30 16:39:10 -07:00
Alessio Gravili
35e5be8558 fix(ui): client should add back default values for valid and passesCondition form field properties (#10709)
As a result of #9388, the `valid` and `passesCondition` properties no
longer appear in form state. This leads to breaking logic if you were
previously relying on these properties to have explicit values. To fix
this, we simply perform the inverse on these properties before accepting
them into client side form state. In the next major release, we can
accept form state as it is received and instruct users to modify their
logic as needed.

Also comes with a small perf optimization, by keeping the old object
reference of fields if they did not change when server form state comes
back
2025-01-30 21:21:31 +00:00
Jarrod Flesch
398589397e fix(ui): revert unrelated code (#10897)
### What?
Reverts mixed code written for #10825 that accidentally made it into
#10888
2025-01-30 15:58:03 -05:00
Jacob Fletcher
c7ad46c2ac chore(deps): deprecates body-scroll-lock 2025-01-30 15:32:57 -05:00
Jacob Fletcher
8a79e59855 chore(templates): improves and simplifies draft preview (#10895)
Similar to #10876. There were a number of things wrong or in need of
improvement with the Draft Preview implementation of the Website
Template, namely:
- The preview secret was missing entirely, with pointless logic was
written to throw an error if it missing in the search params as opposed
to not matching the environment secret. This will ensure that only admin
users, not _any_ user, can enter into preview mode.
- The preview endpoint was unnecessarily querying the database for a
matching document as opposed to letting the underlying page itself 404
as needed, and it was also throwing an inaccurate error message. The
preview route already checks that the path is relative, so there is no
security risk of redirecting to another domain.
- The `/next/exit-preview` route was duplicated twice.
- The logic to format search params in the preview URL was unnecessarily
complex.
2025-01-30 15:01:18 -05:00
David Murdoch
ebb51731f6 templates: remove unknown CSS values (#10891)
* set font-size to unset
* set font-weight to unset

### What?

Change CSS values in global.css files in 3 examples

### Why?

Apparently, the CSS value of `auto` does not actually exist in CSS for
`font-size` and `font-weight`
[mdn](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#syntax)
.
[Stylelint](https://stylelint.io/user-guide/rules/declaration-property-value-no-unknown/)
errors made me aware of this. That rule's description is not specific to
`font-size` and `font-weight`.

This is how it looked in the terminal:

```
src/app/(frontend)/globals.css
  12:18  ✖  Unexpected unknown value "auto" for property "font-weight"  declaration-property-value-no-unknown
  13:16  ✖  Unexpected unknown value "auto" for property "font-size"    declaration-property-value-no-unknown
```

### Fixes:

Change `auto` to `unset` since it uses `initial` styles unless the
heading CSS values have been changed by a parent html tag. I'm guessing
this was reset due to tailwind interrupting this somehow.
2025-01-30 14:54:20 -05:00
Jarrod Flesch
be790a9de2 feat(plugin-multi-tenant): allow opting out of tenant access control merge (#10888)
### What?
In some cases you may want to opt out of using the default access
control that this plugin provides on the tenants collection.

### Why?
Other collections are able to opt out of this already, but the tenants
collection specifically was not configured with an opt out capability.

### How?
Adds new property to the plugin config: `useTenantsCollectionAccess`.
Setting this to `false` allows users to opt out and write their own
access control functions without the plugin merging in its own
constraints for the tenant collection.

Fixes https://github.com/payloadcms/payload/issues/10882
2025-01-30 14:49:19 -05:00
Alessio Gravili
85c0842444 fix(ui): error in version view if document contains localized arrays or blocks (#10893)
Fixes https://github.com/payloadcms/payload/issues/10884
2025-01-30 19:45:47 +00:00
Jacob Fletcher
2b9ee62fc0 chore(examples): misc improvements to the draft preview example (#10876)
There were a number of things wrong or could have been improved with the
[Draft Preview
Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview),
namely:

- The package.json was missing `"type": "modue"` which would throw ESM
related import errors on startup
- The preview secret was missing entirely, with pointless logic was
written to throw an error if it missing in the search params as opposed
to not matching the environment secret
- The `/next/exit-preview` route was duplicated twice
- The preview endpoint was unnecessarily querying the database for a
matching document as opposed to letting the underlying page itself 404
as needed, and it was also throwing an inaccurate error message

Some less critical changes were:
- The page query was missing the `depth` and `limit` parameters which is
best practice to optimize performance
- The logic to format search params in the preview URL was unnecessarily
complex
- Utilities like `generatePreviewPath` and `getGlobals` were
unnecessarily obfuscating simple functions
- The `/preview` and `/exit-preview` routes were unecessarily nested
within a `/next` page segment
- Payload types weren't aliased
2025-01-29 23:14:08 -05:00
Pavel B.
8f27f85023 docs: fix typo on overview.mdx (#10877)
Remove repeated `developers` word.

### What?
There was a typo on the plugins overview page, where `developers
developers` was used twice in a row. Mb that was a quote from Steve
Balmer idk.

### Why?
Docs should be pristine.

### How?
Removed the word.
2025-01-29 19:20:17 -07:00
Jacob Fletcher
d7c3b4e17a docs: admin preview and draft preview (#10875)
Thoroughly documents the `admin.preview` feature. Previously, this
information was briefly mentioned in two distinct places, within the
collections config and again within the globals config. This led to
discrepancies over time and was inadequate at describing this feature,
such as having a lack of concrete code examples especially as it relates
to _draft preview_. There has also been confusion between this and Live
Preview.

Now, there is a dedicated page at `/admin/preview` which centralizes
this information into a single document. It also specifically documents
how to achieve _draft preview_ and includes code snippets. This way, we
no longer have to rely solely on the [Draft Preview
Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview)
for this.

Related: #10798
2025-01-29 18:17:23 -05:00
Amelia
7d429f8b65 feat: adds auto resize feature to textarea (#10786)
This PR introduces an auto resize feature for the `textarea` field. 
By default Payload `textarea` field will dynamically [adjust its height
based on its
content](https://github.com/payloadcms/payload/pull/10786#discussion_r1928961885).

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-01-29 21:40:39 +00:00
Robert Clancy (Robbo)
9638dbe52b fix(plugin-multi-tenant): fixed hardcoded user tenants field (#10782)
### What?
When using custom slugs and field names the tenancy field added to the
users would still attempt to use `tenants` and fail.

### Why?
The tenant/tenancy are hardcoded in `tenantsArrayField()`

### How?
Added the same args that are used in `tenantsField()` for the field
names and relation.
2025-01-29 13:27:00 -05:00
Sasha
2f66bdc2dc fix(ui): create-first-user crashes when users collection has join field (#10871)
Fixes https://github.com/payloadcms/payload/issues/10870
Now we hide join fields from the `/create-first-user` view since they're
not meaningful there.
2025-01-29 19:52:22 +02:00
Elliot DeNolf
5bd17cc111 chore(release): v3.20.0 [skip ci] 2025-01-29 10:41:55 -05:00
Germán Jabloñski
0e5ff246b2 fix(richtext-lexical): preserve selection in Firefox when using LexicalMenu (#10867)
Fixes #10724

The selection is never touched in an `editor.read`, but BEFORE starting
an `editor.update` it is synced with `window.selection`. Firefox for
some reason loses the editor selection, so on the next update the
selection is null.

For reference, there was a brief discussion on the Lexical Discord
server:
https://discord.com/channels/953974421008293909/1333916489870348309
2025-01-29 15:18:24 +00:00
Sasha
3094c92ef3 templates: fix compatibility with pnpm 10 (#10830)
Fixes https://github.com/payloadcms/payload/issues/10813
In pnpm 10 (which isn't "latest" yet), according to the [list of
breaking changes](https://github.com/orgs/pnpm/discussions/8945):
> Lifecycle scripts of dependencies are not executed during installation
by default! This is a breaking change aimed at increasing security. In
order to allow lifecycle scripts of specific dependencies, they should
be listed in the pnpm.onlyBuiltDependencies field of package.json

The sharp package uses a script to install native binaries and so our
templates don't run out of the box with pnpm 10.
2025-01-29 15:58:10 +02:00
Elliot DeNolf
c08f012211 chore(cpa): re-pin template versions (#10857)
Pin create-payload-app to pull latest release versions of templates.
2025-01-29 08:55:29 -05:00
Seno
a47139acfa docs: add missing full stop, fix SlateNodeConverter import (#10860)
- Adding full stop to match other words
- In `@payloadcms/richtext-lexical` – `v3.19.0` SlateNodeConverter is
not imported from `@payloadcms/richtext-lexical/migrate` but rather from
`@payloadcms/richtext-lexical`
2025-01-28 22:07:49 -07:00
Alessio Gravili
219a369603 templates: fix website template not building (#10858)
After our 3.20.0 release, we can remove the `as any` assertion again.

Fixes https://github.com/payloadcms/payload/issues/10840
2025-01-29 04:39:26 +00:00
Germán Jabloñski
c75c6ce6c9 chore(templates): update missing changes in vercel website template (#10827)
This PR migrates some changes that had been made to the website template
and had not been ported to the website template with vercel.

Ideally, so that this does not happen again in the future and we do not
have to do this manually, we could have a script in CI.
2025-01-29 03:39:47 +00:00
Germán Jabloñski
52f86c7780 chore(templates): fix eslint errors in vercel templates (#10768) 2025-01-29 03:14:41 +00:00
Alessio Gravili
c562fbfa94 feat(ui): allows customizing version diff components, render versions ui on the server (#10815)
This PR moves the logic for rendering diff field components in the
version comparison view from the client to the server.

This allows us to expose more customization options to the server-side
Payload Config. For example, users can now pass their own diff
components for fields - even including RSCs.

This PR also cleans up the version view types

Implements the following from
https://github.com/payloadcms/payload/discussions/4197:
- allow for customization of diff components
- more control over versions screens in general

TODO:
- [x] Bring getFieldPaths fixes into core
- [x] Cleanup and test with scrutiny. Ensure all field types display
their diffs correctly
- [x] Review public API for overriding field types, add docs
- [x] Add e2e test for new public API
2025-01-28 22:17:24 +00:00
Tsemach Hadad
33ac13df28 feat(ui): toggle showing only modified fields in version diff view (#10807)
## Description

As an author reviewing the versions I have for a document , I would like
to the ability to focus only on the differences I made and not see the
entire document.
[Screencast from 2024-09-05
16-38-40.webm](https://github.com/user-attachments/assets/25d44a51-bcac-47d5-a2ec-cadae4d108d4)

A checkbox was added to the Version View allowing user to decide if
he/she wants to see only modified fields or the entire documents.
#7981 - mention this feature and also in discord

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

## Type of change

<!-- Please delete options that are not relevant. -->

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

## Checklist:

- [  ] Existing test suite passes locally with my changes
(Actually it's stuck on S3 upload test , note related to my code)

One lat question - should we really translate text for all locales ? or
we can leave it undefined for now ?(besides english)

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-01-28 21:36:07 +00:00
Patrik
989140b992 fix(ui): adds title attribute to Logout button for tooltip (#10851)
Native tooltip was missing from the `Logout` button because it was
missing the `title` attribute.

Adds `title` attribute to the `Logout` button to display native tooltip.

![Screenshot 2025-01-28 at 2 11
07 PM](https://github.com/user-attachments/assets/01f42877-8e01-4cd2-a064-e6c6eb77f216)

Fixes #10773
2025-01-28 14:36:09 -05:00
Said Akhrarov
8a6d995425 fix(ui): correctly reset blocksDrawer search state after close (#10847)
### What?
This PR fixes an issue where after closing the `BlocksDrawer` component
after performing a search, stale `Blocks` were shown the next time it
was open.

### Why?
To properly show all blocks when the `BlocksDrawer` is open after being
closed with a filtered search.

### How?
The `BlocksDrawer` was simply checking the existence of the
`isModalOpen` function instead of calling it as expected.

Fixes #10843

Before:

https://github.com/user-attachments/assets/5f41012d-ca84-41b4-9861-d5e0cb2579f6


After:

[Editing---Block-Field-after--Payload.webm](https://github.com/user-attachments/assets/4bd1ab11-f9a0-438f-a2e6-2ff0aba3e53d)

---------

Co-authored-by: Patrik Kozak <patrik@payloadcms.com>
2025-01-28 13:21:42 -05:00
Patrik
e65a04a20e templates: adds landing page to blank template (#10769)
This addition enhances the `Blank` template by adding a simple front-end
to ensure a better out-of-the-box experience.

When deploying the template to platforms like `Payload Cloud`, `Vercel`,
or similar services, users would previously encounter a `404` or
`not-found` page on the front-end `/` route unless explicitly handled.

With this update, the template now includes a minimal front-end that
renders a basic page at route `/`.

### Notes

- The added front-end is entirely optional.

- If users prefer to use the `Blank` template as a starting point for a
back-end-only solution or plan to integrate with a different front-end
framework, they can simply delete the `(frontend)` folder and proceed as
before.

`Logged out`:

![Screenshot 2025-01-28 at 10 26
01 AM](https://github.com/user-attachments/assets/f6cd99bd-9746-4d0e-910f-2322a671c6b3)

`Logged in`:

![Screenshot 2025-01-28 at 10 25
42 AM](https://github.com/user-attachments/assets/27c0bbfb-bd94-4e3c-9bb9-332aa3ccc8cc)

`Mobile`:

![Screenshot 2025-01-28 at 10 25
14 AM](https://github.com/user-attachments/assets/370869b4-c5e5-4b17-bff6-3514e7baffc7)
2025-01-28 11:39:29 -05:00
Jacob Fletcher
57f72185f8 chore(deps): upgrades react-diff-viewer-continued to v4.0.4 to suppress react 19 warnings and use ESM imports (#10834)
The `react-diff-viewer-continued` package now includes React 19 in its
peer dependencies thanks to
https://github.com/Aeolun/react-diff-viewer-continued/pull/56. This new
version also exports as ESM by default ftw.
2025-01-28 11:31:33 -05:00
347 changed files with 21499 additions and 19403 deletions

View File

@@ -18,7 +18,7 @@
},
"devDependencies": {
"@octokit/webhooks-types": "^7.5.1",
"@swc/jest": "^0.2.36",
"@swc/jest": "^0.2.37",
"@types/jest": "^27.5.2",
"@types/node": "^20.16.5",
"@typescript-eslint/eslint-plugin": "^4.33.0",

114
.github/pnpm-lock.yaml generated vendored
View File

@@ -19,8 +19,8 @@ importers:
specifier: ^7.5.1
version: 7.5.1
'@swc/jest':
specifier: ^0.2.36
version: 0.2.36(@swc/core@1.7.26)
specifier: ^0.2.37
version: 0.2.37(@swc/core@1.7.26)
'@types/jest':
specifier: ^27.5.2
version: 27.5.2
@@ -48,9 +48,6 @@ importers:
prettier:
specifier: ^3.3.3
version: 3.3.3
ts-jest:
specifier: ^26.5.6
version: 26.5.6(jest@29.7.0(@types/node@20.16.5))(typescript@4.9.5)
typescript:
specifier: ^4.9.5
version: 4.9.5
@@ -68,8 +65,8 @@ importers:
specifier: ^7.5.1
version: 7.5.1
'@swc/jest':
specifier: ^0.2.36
version: 0.2.36(@swc/core@1.7.26)
specifier: ^0.2.37
version: 0.2.37(@swc/core@1.7.26)
'@types/jest':
specifier: ^27.5.2
version: 27.5.2
@@ -97,9 +94,6 @@ importers:
prettier:
specifier: ^3.3.3
version: 3.3.3
ts-jest:
specifier: ^26.5.6
version: 26.5.6(jest@29.7.0(@types/node@20.16.5))(typescript@4.9.5)
typescript:
specifier: ^4.9.5
version: 4.9.5
@@ -386,10 +380,6 @@ packages:
resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
'@jest/types@26.6.2':
resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==}
engines: {node: '>= 10.14.2'}
'@jest/types@29.6.3':
resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -542,8 +532,8 @@ packages:
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/jest@0.2.36':
resolution: {integrity: sha512-8X80dp81ugxs4a11z1ka43FPhP+/e+mJNXJSxiNYk8gIX/jPBtY4gQTrKu/KIoco8bzKuPI5lUxjfLiGsfvnlw==}
'@swc/jest@0.2.37':
resolution: {integrity: sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==}
engines: {npm: '>= 7.0.0'}
peerDependencies:
'@swc/core': '*'
@@ -590,9 +580,6 @@ packages:
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
'@types/yargs@15.0.19':
resolution: {integrity: sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==}
'@types/yargs@17.0.33':
resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
@@ -746,10 +733,6 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
bs-logger@0.2.6:
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
engines: {node: '>= 6'}
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
@@ -783,9 +766,6 @@ packages:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
ci-info@2.0.0:
resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
@@ -1133,10 +1113,6 @@ packages:
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-ci@2.0.0:
resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==}
hasBin: true
is-core-module@2.15.1:
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
engines: {node: '>= 0.4'}
@@ -1311,10 +1287,6 @@ packages:
resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
jest-util@26.6.2:
resolution: {integrity: sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==}
engines: {node: '>= 10.14.2'}
jest-util@29.7.0:
resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -1414,9 +1386,6 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
@@ -1438,11 +1407,6 @@ packages:
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1752,14 +1716,6 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
ts-jest@26.5.6:
resolution: {integrity: sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==}
engines: {node: '>= 10'}
hasBin: true
peerDependencies:
jest: '>=26 <27'
typescript: '>=3.8 <5.0'
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
@@ -1863,10 +1819,6 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -2307,14 +2259,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@jest/types@26.6.2':
dependencies:
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 20.16.5
'@types/yargs': 15.0.19
chalk: 4.1.2
'@jest/types@29.6.3':
dependencies:
'@jest/schemas': 29.6.3
@@ -2477,7 +2421,7 @@ snapshots:
'@swc/counter@0.1.3': {}
'@swc/jest@0.2.36(@swc/core@1.7.26)':
'@swc/jest@0.2.37(@swc/core@1.7.26)':
dependencies:
'@jest/create-cache-key-function': 29.7.0
'@swc/core': 1.7.26
@@ -2538,10 +2482,6 @@ snapshots:
'@types/yargs-parser@21.0.3': {}
'@types/yargs@15.0.19':
dependencies:
'@types/yargs-parser': 21.0.3
'@types/yargs@17.0.33':
dependencies:
'@types/yargs-parser': 21.0.3
@@ -2742,10 +2682,6 @@ snapshots:
node-releases: 2.0.18
update-browserslist-db: 1.1.0(browserslist@4.23.3)
bs-logger@0.2.6:
dependencies:
fast-json-stable-stringify: 2.1.0
bser@2.1.1:
dependencies:
node-int64: 0.4.0
@@ -2773,8 +2709,6 @@ snapshots:
char-regex@1.0.2: {}
ci-info@2.0.0: {}
ci-info@3.9.0: {}
cjs-module-lexer@1.4.1: {}
@@ -3127,10 +3061,6 @@ snapshots:
is-arrayish@0.2.1: {}
is-ci@2.0.0:
dependencies:
ci-info: 2.0.0
is-core-module@2.15.1:
dependencies:
hasown: 2.0.2
@@ -3470,15 +3400,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
jest-util@26.6.2:
dependencies:
'@jest/types': 26.6.2
'@types/node': 20.16.5
chalk: 4.1.2
graceful-fs: 4.2.11
is-ci: 2.0.0
micromatch: 4.0.8
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
@@ -3583,8 +3504,6 @@ snapshots:
dependencies:
semver: 7.6.3
make-error@1.3.6: {}
makeerror@1.0.12:
dependencies:
tmpl: 1.0.5
@@ -3604,8 +3523,6 @@ snapshots:
dependencies:
brace-expansion: 1.1.11
mkdirp@1.0.4: {}
ms@2.1.3: {}
natural-compare@1.4.0: {}
@@ -3859,21 +3776,6 @@ snapshots:
tree-kill@1.2.2: {}
ts-jest@26.5.6(jest@29.7.0(@types/node@20.16.5))(typescript@4.9.5):
dependencies:
bs-logger: 0.2.6
buffer-from: 1.1.2
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@20.16.5)
jest-util: 26.6.2
json5: 2.2.3
lodash: 4.17.21
make-error: 1.3.6
mkdirp: 1.0.4
semver: 7.6.3
typescript: 4.9.5
yargs-parser: 20.2.9
tslib@1.14.1: {}
tslib@2.7.0: {}
@@ -3959,8 +3861,6 @@ snapshots:
yallist@3.1.1: {}
yargs-parser@20.2.9: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:

37
.vscode/settings.json vendored
View File

@@ -1,34 +1,9 @@
{
"npm.packageManager": "pnpm",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.formatOnSaveMode": "file",
"eslint.rules.customizations": [
@@ -43,12 +18,6 @@
"typescript.tsdk": "node_modules/typescript/lib",
// Load .git-blame-ignore-revs file
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
"[javascript][typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"files.insertFinalNewline": true,
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
"jestrunner.debugOptions": {
"runtimeArgs": ["--no-deprecation"]

View File

@@ -1,7 +1,7 @@
---
title: Swap in your own React components
label: Custom Components
order: 40
order: 20
desc: Fully customize your Admin Panel by swapping in your own React components. Add fields, remove views, update routes and change functions to sculpt your perfect Dashboard.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---

View File

@@ -1,7 +1,7 @@
---
title: React Hooks
label: React Hooks
order: 70
order: 40
desc: Make use of all of the powerful React hooks that Payload provides.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---

View File

@@ -6,7 +6,7 @@ desc: Manage your data and customize the Payload Admin Panel by swapping in your
keywords: admin, components, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload dynamically generates a beautiful, [fully type-safe](../typescript/overview) Admin Panel to manage your users and data. It is highly performant, even with 100+ fields, and is translated in over 30 languages. Within the Admin Panel you can manage content, [render your site](../live-preview/overview), preview drafts, [diff versions](../versions/overview), and so much more.
Payload dynamically generates a beautiful, [fully type-safe](../typescript/overview) Admin Panel to manage your users and data. It is highly performant, even with 100+ fields, and is translated in over 30 languages. Within the Admin Panel you can manage content, [render your site](../live-preview/overview), [preview drafts](./preview), [diff versions](../versions/overview), and so much more.
The Admin Panel is designed to [white-label your brand](https://payloadcms.com/blog/white-label-admin-ui). You can endlessly customize and extend the Admin UI by swapping in your own [Custom Components](./components)—everything from simple field labels to entire views can be modified or replaced to perfectly tailor the interface for your editors.

217
docs/admin/preview.mdx Normal file
View File

@@ -0,0 +1,217 @@
---
title: Preview
label: Preview
order: 50
desc: Enable links to your front-end to preview published or draft content.
keywords: admin, components, preview, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Preview is a feature that allows you to generate a direct link to your front-end application. When enabled, a "preview" button will appear on the Edit View within the [Admin Panel](./overview) with an href pointing to the URL you provide. This will provide your editors with a quick way of navigating to the front-end application where that Document's data is represented. Otherwise, they'd have to determine that URL themselves which is not always straightforward especially in complex apps.
The Preview feature can also be used to achieve something known as "Draft Preview". With Draft Preview, you can navigate to your front-end application and enter "draft mode", where your queries are modified to fetch draft content instead of published content. This is useful for seeing how your content will look before being published. [More details](#draft-preview).
<Banner type="warning">
**Note:**
Preview is different than [Live Preview](../live-preview/overview). Live Preview loads your app within an iframe and renders it in the Admin Panel allowing you to see changes in real-time. Preview, on the other hand, allows you to generate a direct link to your front-end application.
</Banner>
To add Preview, pass a function to the `admin.preview` property in any [Collection Config](../configuration/collections#admin-options) or [Global Config](../configuration/globals#admin-options):
```ts
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
preview: ({ slug }) => `http://localhost:3000/${slug}`,
},
fields: [
{
name: 'slug',
type: 'text',
}
],
}
```
## Options
The `preview` function resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path, and can run async if needed.
The following arguments are provided to the `preview` function:
| Path | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| **`doc`** | The data of the Document being edited. This includes changes that have not yet been saved. |
| **`options`** | An object with additional properties. |
The `options` object contains the following properties:
| Path | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| **`locale`** | The current locale of the Document being edited. |
| **`req`** | The Payload Request object. |
| **`token`** | The JWT token of the currently authenticated in user. |
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
```ts
preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
```
## Draft Preview
The Preview feature can be used to achieve "Draft Preview". After clicking the preview button from the Admin Panel, you can enter into "draft mode" within your front-end application. This will allow you to adjust your page queries to include the `draft: true` param. When this param is present on the request, Payload will send back a draft document as opposed to a published one based on the document's `_status` field.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here very from framework to framework although the underlying concept is the same.
### Next.js
If you're using Next.js, you can do the following code to enter [Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode).
#### Step 1: Format the Preview URL
First, format your `admin.preview` function to point to a custom endpoint that you'll open on your front-end. This URL should include a few key query search params:
```ts
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
preview: ({ slug, collection }) => {
const encodedParams = new URLSearchParams({
slug,
collection,
path: `/${slug}`,
previewSecret: process.env.PREVIEW_SECRET || ''
})
return `/preview?${encodedParams.toString()}` // highlight-line
}
},
fields: [
{
name: 'slug',
type: 'text',
}
],
}
```
#### Step 2: Create the Preview Route
Then, create an API route that verifies the preview secret, authenticates the user, and enters draft mode:
`/app/preview/route.ts`
```ts
import type { CollectionSlug, PayloadRequest } from 'payload'
import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import configPromise from '@payload-config'
export async function GET(
req: {
cookies: {
get: (name: string) => {
value: string
}
}
} & Request,
): Promise<Response> {
const payload = await getPayload({ config: configPromise })
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
const collection = searchParams.get('collection') as CollectionSlug
const slug = searchParams.get('slug')
const previewSecret = searchParams.get('previewSecret')
if (previewSecret !== process.env.PREVIEW_SECRET) {
return new Response('You are not allowed to preview this page', { status: 403 })
}
if (!path || !collection || !slug) {
return new Response('Insufficient search params', { status: 404 })
}
if (!path.startsWith('/')) {
return new Response('This endpoint can only be used for relative previews', { status: 500 })
}
let user
try {
user = await payload.auth({
req: req as unknown as PayloadRequest,
headers: req.headers,
})
} catch (error) {
payload.logger.error({ err: error }, 'Error verifying token for live preview')
return new Response('You are not allowed to preview this page', { status: 403 })
}
const draft = await draftMode()
if (!user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
// You can add additional checks here to see if the user is allowed to preview this page
draft.enable()
redirect(path)
}
```
#### Step 3: Query Draft Content
Finally, in your front-end application, you can detect draft mode and adjust your queries to include drafts:
`/app/[slug]/page.tsx`
```ts
export default async function Page({ params: paramsPromise }) {
const { slug = 'home' } = await paramsPromise
const { isEnabled: isDraftMode } = await draftMode()
const payload = await getPayload({ config })
const page = await payload.find({
collection: 'pages',
depth: 0,
draft: isDraftMode, // highlight-line
limit: 1,
overrideAccess: isDraftMode,
where: {
slug: {
equals: slug,
},
},
})?.then(({ docs }) => docs?.[0])
if (page === null) {
return notFound()
}
return (
<main>
<h1>{page?.title}</h1>
</main>
)
}
```
<Banner type="success">
**Note:**
For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples).
</Banner>

View File

@@ -1,7 +1,7 @@
---
title: Customizing Views
label: Customizing Views
order: 50
order: 30
desc:
keywords:
---
@@ -289,7 +289,7 @@ The following options are available:
### Document Tabs
Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `tab` key:
Each Custom View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `tab` key:
```ts
import type { SanitizedCollectionConfig } from 'payload'

View File

@@ -124,7 +124,7 @@ The following options are available:
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| **`enableRichTextRelationship`** | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| **`meta`** | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](#preview). |
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
@@ -162,7 +162,7 @@ The following options are available:
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
| **`edit.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
| **`edit.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](#preview) must be enabled. |
| **`edit.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](../admin/preview) must be enabled. |
| **`edit.Upload`** | Replace the default Upload component with a Custom Component. [Upload](../upload/overview) must be enabled. |
| **`views`** | Override or create new views within the Admin Panel. [More details](../admin/views). |
@@ -171,51 +171,6 @@ The following options are available:
For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).
</Banner>
### Preview
It is possible to display a Preview Button within the Edit View of the Admin Panel. This will allow editors to visit the frontend of your app the corresponds to the document they are actively editing. This way they can preview the latest, potentially unpublished changes.
To configure the Preview Button, set the `admin.preview` property to a function in your Collection Config:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
// ...
admin: {
// highlight-start
preview: (doc, { locale }) => {
if (doc?.slug) {
return `/${doc.slug}?locale=${locale}`
}
return null
},
// highlight-end
},
}
```
The `preview` property resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path.
The preview function receives two arguments:
| Argument | Description |
| --- | --- |
| **`doc`** | The Document being edited. |
| **`ctx`** | An object containing `locale`, `token`, and `req` properties. The `token` is the currently logged-in user's JWT. |
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
```ts
preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
```
<Banner type="success">
**Note:**
For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples).
</Banner>
### Pagination
All Collections receive their own List View which displays a paginated list of documents that can be sorted and filtered. The pagination behavior of the List View can be customized on a per-Collection basis, and uses the same [Pagination](../queries/pagination) API that Payload provides.

View File

@@ -120,7 +120,7 @@ The following options are available:
| **`group`** | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Global from navigation and admin routing. |
| **`components`** | Swap in your own React components to be used within this Global. [More details](#custom-components). |
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](#preview). |
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](../admin/preview). |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this collection. |
| **`meta`** | Page metadata overrides to apply to this Global within the Admin Panel. [More details](../admin/metadata). |
@@ -151,7 +151,7 @@ The following options are available:
| **`elements.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`elements.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
| **`elements.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
| **`elements.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](#preview) must be enabled. |
| **`elements.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](../admin/preview) must be enabled. |
| **`views`** | Override or create new views within the Admin Panel. [More details](../admin/views). |
<Banner type="success">
@@ -159,43 +159,6 @@ The following options are available:
For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).
</Banner>
### Preview
It is possible to display a Preview Button within the Edit View of the Admin Panel. This will allow editors to visit the frontend of your app the corresponds to the document they are actively editing. This way they can preview the latest, potentially unpublished changes.
To configure the Preview Button, set the `admin.preview` property to a function in your Global Config:
```ts
import { GlobalConfig } from 'payload'
export const MainMenu: GlobalConfig = {
// ...
admin: {
// highlight-start
preview: (doc, { locale }) => {
if (doc?.slug) {
return `/${doc.slug}?locale=${locale}`
}
return null
},
// highlight-end
},
}
```
The preview function receives two arguments:
| Argument | Description |
| --- | --- |
| **`doc`** | The Document being edited. |
| **`ctx`** | An object containing `locale` and `token` properties. The `token` is the currently logged-in user's JWT. |
<Banner type="success">
**Note:**
For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples).
</Banner>
## GraphQL
You can completely disable GraphQL for this global by passing `graphQL: false` to your global config. This will completely disable all queries, mutations, and types from appearing in your GraphQL schema.

View File

@@ -1,9 +1,9 @@
---
title: Fields Overview
description: Fields are the building blocks of Payload, find out how to add or remove a field, change field type, add hooks, define Access Control and Validation.
keywords: overview, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
label: Overview
order: 10
desc: Fields are the building blocks of Payload, find out how to add or remove a field, change field type, add hooks, define Access Control and Validation.
keywords: overview, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
title: Fields Overview
---
Fields are the building blocks of Payload. They define the schema of the Documents that will be stored in the [Database](../database/overview), as well as automatically generate the corresponding UI within the [Admin Panel](../admin/overview).
@@ -48,8 +48,7 @@ export const Page: CollectionConfig = {
```
<Banner type="warning">
**Reminder:**
Each field is an object with at least the `type` property. This matches the field to its corresponding Field Type. [More details](#field-options).
**Reminder:** Each field is an object with at least the `type` property. This matches the field to its corresponding Field Type. [More details](#field-options).
</Banner>
There are three main categories of fields in Payload:
@@ -91,10 +90,10 @@ Presentational Fields do not store data in the database. Instead, they are used
Here are the available Presentational Fields:
- [Collapsible](/docs/fields/collapsible) - nests fields within a collapsible component
- [Row](/docs/fields/row) - aligns fields horizontally
- [Tabs (Unnamed)](/docs/fields/tabs) - nests fields within a tabbed layout
- [UI](/docs/fields/ui) - blank field for custom UI components
- [Collapsible](../fields/collapsible) - nests fields within a collapsible component
- [Row](../fields/row) - aligns fields horizontally
- [Tabs (Unnamed)](../fields/tabs) - nests fields within a tabbed layout
- [UI](../fields/ui) - blank field for custom UI components
### Virtual Fields
@@ -102,11 +101,10 @@ Virtual fields are used to display data that is not stored in the database. They
Here are the available Virtual Fields:
- [Join](/docs/fields/join) - achieves two-way data binding between fields
- [Join](../fields/join) - achieves two-way data binding between fields
<Banner type="success">
**Tip:**
Don't see a built-in field type that you need? Build it! Using a combination of [Field Validations](#validation) and [Custom Components](../admin/components), you can override the entirety of how a component functions within the [Admin Panel](../admin/overview) to effectively create your own field type.
**Tip:** Don't see a built-in field type that you need? Build it! Using a combination of [Field Validations](#validation) and [Custom Components](../admin/components), you can override the entirety of how a component functions within the [Admin Panel](../admin/overview) to effectively create your own field type.
</Banner>
## Field Options
@@ -147,10 +145,10 @@ Payload reserves various field names for internal use. Using reserved field name
The following field names are forbidden and cannot be used:
- `__v`
- `salt`
- `hash`
- `file`
- `__v`
- `salt`
- `hash`
- `file`
### Field-level Hooks
@@ -241,8 +239,7 @@ export const myField: Field = {
```
<Banner type="success">
**Tip:**
You can use async `defaultValue` functions to fill fields with data from API requests or Local API using `req.payload`.
**Tip:** You can use async `defaultValue` functions to fill fields with data from API requests or Local API using `req.payload`.
</Banner>
### Validation
@@ -265,10 +262,10 @@ Custom validation functions should return either `true` or a `string` representi
The following arguments are provided to the `validate` function:
| Argument | Description |
| -------- | --------------------------------------------------------------------------------------------- |
| `value` | The value of the field being validated. |
| `ctx` | An object with additional data and context. [More details](#validation-context) |
| Argument | Description |
| --- | --- |
| `value` | The value of the field being validated. |
| `ctx` | An object with additional data and context. [More details](#validation-context) |
#### Validation Context
@@ -289,14 +286,14 @@ export const MyField: Field = {
The following additional properties are provided in the `ctx` object:
| Property | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `data` | An object containing the full collection or global document currently being edited. |
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
| Property | Description |
| --- | --- |
| `data` | An object containing the full collection or global document currently being edited. |
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
#### Reusing Default Field Validations
@@ -402,8 +399,7 @@ export const MyCollection: CollectionConfig = {
```
<Banner type="warning">
**Reminder:**
The Custom ID Fields can only be of type [`Number`](./number) or [`Text`](./text). Custom ID fields with type `text` must not contain `/` or `.` characters.
**Reminder:** The Custom ID Fields can only be of type [`Number`](./number) or [`Text`](./text). Custom ID fields with type `text` must not contain `/` or `.` characters.
</Banner>
## Admin Options
@@ -430,21 +426,21 @@ export const CollectionConfig: CollectionConfig = {
The following options are available:
| Option | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`condition`** | Programmatically show / hide fields based on other fields. [More details](#conditional-logic). |
| **`components`** | All Field Components can be swapped out for [Custom Components](../admin/components) that you define. |
| **`description`** | Helper text to display alongside the field to provide more information for the editor. [More details](#description). |
| **`position`** | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. |
| **`width`** | Restrict the width of a field. You can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. |
| **`style`** | [CSS Properties](https://developer.mozilla.org/en-US/docs/Web/CSS) to inject into the root element of the field. |
| **`className`** | Attach a [CSS class attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) to the root DOM element of a field. |
| **`readOnly`** | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. |
| **`disabled`** | If a field is `disabled`, it is completely omitted from the [Admin Panel](../admin/overview) entirely. |
| **`disableBulkEdit`** | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. Defaults to `true` for UI fields. |
| **`disableListColumn`** | Set `disableListColumn` to `true` to prevent fields from appearing in the list view column selector. |
| **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
| **`hidden`** | Will transform the field into a `hidden` input type. Its value will still submit with requests in the Admin Panel, but the field itself will not be visible to editors. |
| Option | Description |
| --- | --- |
| **`condition`** | Programmatically show / hide fields based on other fields. [More details](#conditional-logic). |
| **`components`** | All Field Components can be swapped out for [Custom Components](../admin/components) that you define. |
| **`description`** | Helper text to display alongside the field to provide more information for the editor. [More details](#description). |
| **`position`** | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. |
| **`width`** | Restrict the width of a field. You can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. |
| **`style`** | [CSS Properties](https://developer.mozilla.org/en-US/docs/Web/CSS) to inject into the root element of the field. |
| **`className`** | Attach a [CSS class attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) to the root DOM element of a field. |
| **`readOnly`** | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. |
| **`disabled`** | If a field is `disabled`, it is completely omitted from the [Admin Panel](../admin/overview) entirely. |
| **`disableBulkEdit`** | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. Defaults to `true` for UI fields. |
| **`disableListColumn`** | Set `disableListColumn` to `true` to prevent fields from appearing in the list view column selector. |
| **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
| **`hidden`** | Will transform the field into a `hidden` input type. Its value will still submit with requests in the Admin Panel, but the field itself will not be visible to editors. |
### Field Descriptions
@@ -452,9 +448,9 @@ Field Descriptions are used to provide additional information to the editor abou
A description can be configured in three ways:
- As a string.
- As a function which returns a string. [More details](#description-functions).
- As a React component. [More details](#description).
- As a string.
- As a function which returns a string. [More details](#description-functions).
- As a React component. [More details](#description).
To add a Custom Description to a field, use the `admin.description` property in your Field Config:
@@ -477,15 +473,14 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
```
<Banner type="warning">
**Reminder:**
To replace the Field Description with a [Custom Component](../admin/components), use the `admin.components.Description` property. [More details](#description).
**Reminder:** To replace the Field Description with a [Custom Component](../admin/components), use the `admin.components.Description` property. [More details](#description).
</Banner>
#### Description Functions
Custom Descriptions can also be defined as a function. Description Functions are executed on the server and can be used to format simple descriptions based on the user's current [Locale](../configuration/localization).
To add a Description Function to a field, set the `admin.description` property to a _function_ in your Field Config:
To add a Description Function to a field, set the `admin.description` property to a *function* in your Field Config:
```ts
import type { SanitizedCollectionConfig } from 'payload'
@@ -507,13 +502,12 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
All Description Functions receive the following arguments:
| Argument | Description |
| -------------- | ---------------------------------------------------------------- |
| **`t`** | The `t` function used to internationalize the Admin Panel. [More details](../configuration/i18n) |
| Argument | Description |
| --- | --- |
| **`t`** | The `t` function used to internationalize the Admin Panel. [More details](../configuration/i18n) |
<Banner type="info">
**Note:**
If you need to subscribe to live updates within your form, use a Description Component instead. [More details](#description).
**Note:** If you need to subscribe to live updates within your form, use a Description Component instead. [More details](#description).
</Banner>
### Conditional Logic
@@ -562,6 +556,7 @@ Within the [Admin Panel](../admin/overview), fields are represented in three dis
- [Field](#field) - The actual form field rendered in the Edit View.
- [Cell](#cell) - The table cell component rendered in the List View.
- [Filter](#filter) - The filter component rendered in the List View.
- [Diff](#diff) - The Diff component rendered in the Version Diff View
To swap in Field Components with your own, use the `admin.components` property in your Field Config:
@@ -586,16 +581,17 @@ export const CollectionConfig: CollectionConfig = {
The following options are available:
| Component | Description |
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| **`Field`** | The form field rendered of the Edit View. [More details](#field). |
| **`Cell`** | The table cell rendered of the List View. [More details](#cell). |
| **`Filter`** | The filter component rendered in the List View. [More details](#filter). |
| **`Label`** | Override the default Label of the Field Component. [More details](#label). |
| **`Error`** | Override the default Error of the Field Component. [More details](#error). |
| **`Description`** | Override the default Description of the Field Component. [More details](#description). |
| **`beforeInput`** | An array of elements that will be added before the input of the Field Component. [More details](#afterinput-and-beforeinput).|
| **`afterInput`** | An array of elements that will be added after the input of the Field Component. [More details](#afterinput-and-beforeinput). |
| Component | Description |
| --- | --- |
| **`Field`** | The form field rendered of the Edit View. [More details](#field). |
| **`Cell`** | The table cell rendered of the List View. [More details](#cell). |
| **`Filter`** | The filter component rendered in the List View. [More details](#filter). |
| **`Label`** | Override the default Label of the Field Component. [More details](#label). |
| **`Error`** | Override the default Error of the Field Component. [More details](#error). |
| **`Diff`** | Override the default Diff component rendered in the Version Diff View. [More details](#diff). |
| **`Description`** | Override the default Description of the Field Component. [More details](#description). |
| **`beforeInput`** | An array of elements that will be added before the input of the Field Component. [More details](#afterinput-and-beforeinput). |
| **`afterInput`** | An array of elements that will be added after the input of the Field Component. [More details](#afterinput-and-beforeinput). |
#### Field
@@ -622,7 +618,7 @@ export const CollectionConfig: CollectionConfig = {
}
```
_For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components)._
*For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).*
<Banner type="warning">
Instead of replacing the entire Field Component, you can alternately replace or slot-in only specific parts by using the [`Label`](#label), [`Error`](#error), [`beforeInput`](#afterinput-and-beforinput), and [`afterInput`](#afterinput-and-beforinput) properties.
@@ -632,31 +628,31 @@ _For details on how to build Custom Components, see [Building Custom Components]
All Field Components receive the following props by default:
| Property | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`docPreferences`** | An object that contains the [Preferences](../admin/preferences) for the document. |
| **`field`** | In Client Components, this is the sanitized Client Field Config. In Server Components, this is the original Field Config. Server Components will also receive the sanitized field config through the`clientField` prop (see below). |
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`validate`** | A function that can be used to validate the field. |
| **`path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray.0.myField`. |
| **`schemaPath`** | A string representing the direct, static path to the Field Config, i.e. `posts.myGroup.myArray.myField`. |
| **`indexPath`** | A hyphen-notated string representing the path to the field _within the nearest named ancestor field_, i.e. `0-0` |
| Property | Description |
| --- | --- |
| **`docPreferences`** | An object that contains the [Preferences](../admin/preferences) for the document. |
| **`field`** | In Client Components, this is the sanitized Client Field Config. In Server Components, this is the original Field Config. Server Components will also receive the sanitized field config through the`clientField` prop (see below). |
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`validate`** | A function that can be used to validate the field. |
| **`path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray.0.myField`. |
| **`schemaPath`** | A string representing the direct, static path to the Field Config, i.e. `posts.myGroup.myArray.myField`. |
| **`indexPath`** | A hyphen-notated string representing the path to the field *within the nearest named ancestor field*, i.e. `0-0` |
In addition to the above props, all Server Components will also receive the following props:
| Property | Description |
| ----------------- | ----------------------------------------------------------------------------- |
| **`clientField`** | The serializable Client Field Config. |
| **`field`** | The Field Config. |
| **`data`** | The current document being edited. |
| **`i18n`** | The [i18n](../configuration/i18n) object. |
| **`payload`** | The [Payload](../local-api/overview) class. |
| **`permissions`** | The field permissions based on the currently authenticated user. |
| **`siblingData`** | The data of the field's siblings. |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`value`** | The value of the field at render-time. |
| Property | Description |
| --- | --- |
| **`clientField`** | The serializable Client Field Config. |
| **`field`** | The Field Config. |
| **`data`** | The current document being edited. |
| **`i18n`** | The [i18n](../configuration/i18n) object. |
| **`payload`** | The [Payload](../local-api/overview) class. |
| **`permissions`** | The field permissions based on the currently authenticated user. |
| **`siblingData`** | The data of the field's siblings. |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`value`** | The value of the field at render-time. |
##### Sending and receiving values from the form
@@ -722,10 +718,10 @@ export const myField: Field = {
All Cell Components receive the same [Default Field Component Props](#field), plus the following:
| Property | Description |
| ---------------- | ----------------------------------------------------------------- |
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
| **`onClick`** | A function that is called when the cell is clicked. |
| Property | Description |
| --- | --- |
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
| **`onClick`** | A function that is called when the cell is clicked. |
For details on how to build Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
@@ -867,9 +863,45 @@ import type {
} from 'payload'
```
#### Diff
The Diff Component is rendered in the Version Diff view. It will only be visible in entities with versioning enabled,
To swap in your own Diff Component, use the `admin.components.Diff` property in your Field Config:
```ts
import type { Field } from 'payload'
export const myField: Field = {
name: 'myField',
type: 'text',
admin: {
components: {
Diff: '/path/to/MyCustomDiffComponent', // highlight-line
},
},
}
```
All Error Components receive the [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
##### TypeScript#diff-component-types
When building Custom Diff Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Diff Component, one for every Field Type and server/client environment. The convention is to append `DiffServerComponent` or `DiffClientComponent` to the type of field, i.e. `TextFieldDiffClientComponent`.
```tsx
import type {
TextFieldDiffServerComponent,
TextFieldDiffClientComponent,
// And so on for each Field Type
} from 'payload'
```
#### afterInput and beforeInput
With these properties you can add multiple components _before_ and _after_ the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component.
With these properties you can add multiple components *before* and *after* the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component.
To add components before and after the input element, use the `admin.components.beforeInput` and `admin.components.afterInput` properties in your Field Config:
@@ -906,4 +938,4 @@ You can import the Payload `Field` type as well as other common types from the `
```ts
import type { Field } from 'payload'
```
```

View File

@@ -28,6 +28,7 @@ const config = buildConfig({
}
})
```
<Banner type="warning">
**Reminder:**
Alternatively, you can define the `admin.livePreview` property on individual [Collection Admin Configs](../configuration/collections#admin-options) and [Global Admin Configs](../configuration/globals#admin-options). Settings defined here will be merged into the top-level as overrides.

View File

@@ -6,7 +6,7 @@ desc: Easily build and manage forms from the Admin Panel. Send dynamic, personal
keywords: plugins, plugin, form, forms, form builder
---
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-form-builder)](https://www.npmjs.com/package/@payloadcms/plugin-form-builder)
![https://www.npmjs.com/package/@payloadcms/plugin-form-builder](https://img.shields.io/npm/v/@payloadcms/plugin-form-builder)
This plugin allows you to build and manage custom forms directly within the [Admin Panel](../admin/overview). Instead of hard-coding a new form into your website or application every time you need one, admins can simply define the schema for each form they need on-the-fly, and your front-end can map over this schema, render its own UI components, and match your brand's design system.

View File

@@ -6,7 +6,7 @@ desc: Scaffolds multi-tenancy for your Payload application
keywords: plugins, multi-tenant, multi-tenancy, plugin, payload, cms, seo, indexing, search, search engine
---
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-multi-tenant)](https://www.npmjs.com/package/@payloadcms/plugin-multi-tenant)
![https://www.npmjs.com/package/@payloadcms/plugin-multi-tenant](https://img.shields.io/npm/v/@payloadcms/plugin-multi-tenant)
This plugin sets up multi-tenancy for your application from within your [Admin Panel](../admin/overview). It does so by adding a `tenant` field to all specified collections. Your front-end application can then query data by tenant. You must add the Tenants collection so you control what fields are available for each tenant.
@@ -25,6 +25,18 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
- Adds a `tenant` field to each specified collection
- Adds a tenant selector to the admin panel, allowing you to switch between tenants
- Filters list view results by selected tenant
- Filters relationship fields by selected tenant
- Ability to create "global" like collections, 1 doc per tenant
- Automatically assign a tenant to new documents
<Banner type="error">
**Warning**
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
strong access control on your tenants collection to prevent deletions by unauthorized users.
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
</Banner>
## Installation
@@ -40,7 +52,7 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
@@ -144,8 +156,12 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user } ? ConfigTypes['user'] : User,
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
) => boolean
/**
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
}
```

View File

@@ -6,7 +6,7 @@ desc: Nested documents in a parent, child, and sibling relationship.
keywords: plugins, nested, documents, parent, child, sibling, relationship
---
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-nested-docs)](https://www.npmjs.com/package/@payloadcms/plugin-nested-docs)
![https://www.npmjs.com/package/@payloadcms/plugin-nested-docs](https://img.shields.io/npm/v/@payloadcms/plugin-nested-docs)
This plugin allows you to easily nest the documents of your application inside of one another. It does so by adding a
new `parent` field onto each of your documents that, when selected, attaches itself to the parent's tree. When you edit

View File

@@ -6,7 +6,7 @@ desc: Plugins provide a great way to modularize Payload functionalities into eas
keywords: plugins, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful is sharing your work across multiple projects or with the greater Payload community.
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful is sharing your work across multiple projects or with the greater Payload community.
There are many [Official Plugins](#official-plugins) available that solve for some of the most common uses cases, such as the [Form Builder Plugin](./form-builder) or [SEO Plugin](./seo). There are also [Community Plugins](#community-plugins) available, maintained entirely by contributing members. To extend Payload's functionality in some other way, you can easily [build your own plugin](./build-your-own).

View File

@@ -6,7 +6,7 @@ desc: Automatically create redirects for your Payload application
keywords: plugins, redirects, redirect, plugin, payload, cms, seo, indexing, search, search engine
---
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-redirects)](https://www.npmjs.com/package/@payloadcms/plugin-redirects)
![https://www.npmjs.com/package/@payloadcms/plugin-redirects](https://img.shields.io/npm/v/@payloadcms/plugin-redirects)
This plugin allows you to easily manage redirects for your application from within your [Admin Panel](../admin/overview). It does so by adding a `redirects` collection to your config that allows you specify a redirect from one URL to another. Your front-end application can use this data to automatically redirect users to the correct page using proper HTTP status codes. This is useful for SEO, indexing, and search engine ranking when re-platforming or when changing your URL structure.

View File

@@ -6,7 +6,7 @@ desc: Generates records of your documents that are extremely fast to search on.
keywords: plugins, search, search plugin, search engine, search index, search results, search bar, search box, search field, search form, search input
---
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-search)](https://www.npmjs.com/package/@payloadcms/plugin-search)
![https://www.npmjs.com/package/@payloadcms/plugin-search](https://img.shields.io/npm/v/@payloadcms/plugin-search)
This plugin generates records of your documents that are extremely fast to search on. It does so by creating a new `search` collection that is indexed in the database then saving a static copy of each of your documents using only search-critical data. Search records are automatically created, synced, and deleted behind-the-scenes as you manage your application's documents.

View File

@@ -6,7 +6,7 @@ desc: Integrate Sentry error tracking into your Payload application
keywords: plugins, sentry, error, tracking, monitoring, logging, bug, reporting, performance
---
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-sentry)](https://www.npmjs.com/package/@payloadcms/plugin-sentry)
![https://www.npmjs.com/package/@payloadcms/plugin-sentry](https://img.shields.io/npm/v/@payloadcms/plugin-sentry)
This plugin allows you to integrate [Sentry](https://sentry.io/) seamlessly with your [Payload](https://github.com/payloadcms/payload) application.

View File

@@ -6,7 +6,7 @@ desc: Easily accept payments with Stripe
keywords: plugins, stripe, payments, ecommerce
---
[![npm](https://img.shields.io/npm/v/@payloadcms/plugin-stripe)](https://www.npmjs.com/package/@payloadcms/plugin-stripe)
![https://www.npmjs.com/package/@payloadcms/plugin-stripe](https://img.shields.io/npm/v/@payloadcms/plugin-stripe)
With this plugin you can easily integrate [Stripe](https://stripe.com) into Payload. Simply provide your Stripe credentials and this plugin will open up a two-way communication channel between the two platforms. This enables you to easily sync data back and forth, as well as proxy the Stripe REST API through Payload's [Access Control](../access-control/overview). Use this plugin to completely offload billing to Stripe and retain full control over your application's data.

View File

@@ -28,7 +28,7 @@ To securely allow headless operation you will need to configure the allowed orig
## Limiting GraphQL Complexity
Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way specify complexity limits which are based on a complexity score that is calculated for each request.
Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way to specify complexity limits. These limits are based on a complexity score calculated for each request.
Any GraphQL request that is calculated to be too expensive is rejected. On the Payload Config, in `graphQL` you can set the `maxComplexity` value as an integer. For reference, the default complexity value for each added field is 1, and all `relationship` and `upload` fields are assigned a value of 10.

View File

@@ -16,7 +16,7 @@ Just import the `migrateSlateToLexical` function we provide, pass it the `payloa
IMPORTANT: This will overwrite all slate data. We recommend doing the following first:
1. Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support.
2. Make every richText field a lexical editor. This script will only convert lexical richText fields with old Slate data
2. Make every richText field a lexical editor. This script will only convert lexical richText fields with old Slate data.
3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the Admin Panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated.
4. If this works as expected, add the `disableHooks: true` prop everywhere you're initializing `SlateToLexicalFeature`. Example: `SlateToLexicalFeature({ disableHooks: true })`. Once you did that, you're ready to run the migration script.
@@ -67,7 +67,7 @@ If you have custom Slate nodes, create a custom converter for them. Here's the U
```ts
import type { SerializedUploadNode } from '../uploadNode'
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical/migrate'
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical'
export const SlateUploadConverter: SlateNodeConverter = {
converter({ slateNode }) {
@@ -90,7 +90,7 @@ export const SlateUploadConverter: SlateNodeConverter = {
It's pretty simple: You get a Slate node as input, and you return the lexical node. The `nodeTypes` array is used to determine which Slate nodes this converter can handle.
When using a migration script, you can add your custom converters to the `converters` property of the `convertSlateToLexical` props, as seen in the example above
When using a migration script, you can add your custom converters to the `converters` property of the `convertSlateToLexical` props, as seen in the example above.
When using the `SlateToLexicalFeature`, you can add your custom converters to the `converters` property of the `SlateToLexicalFeature` props:

View File

@@ -6,3 +6,6 @@ PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Used to validate the preview request
PREVIEW_SECRET=YOUR_SECRET_HERE

View File

@@ -1,6 +1,6 @@
# Payload Draft Preview Example
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published.
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) demonstrates how to implement [Draft Preview](https://payloadcms.com/docs/admin/preview#draft-preview) in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). With Draft Preview, you can navigate to your front-end application and enter "draft mode", where your queries are modified to fetch draft content instead of published content. This is useful for seeing how your content will look before being published.
## Quick Start

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "Payload preview example.",
"license": "MIT",
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import { notFound } from 'next/navigation'
import { getPayload } from 'payload'
import React, { cache, Fragment } from 'react'
import type { Page as PageType } from '../../../payload-types'
import type { Page as PageType } from '@payload-types'
import { Gutter } from '../../../components/Gutter'
import RichText from '../../../components/RichText'
@@ -13,6 +13,7 @@ import classes from './index.module.scss'
export async function generateStaticParams() {
const payload = await getPayload({ config })
const pages = await payload.find({
collection: 'pages',
draft: false,

View File

@@ -1,7 +0,0 @@
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}

View File

@@ -1,94 +0,0 @@
import type { CollectionSlug, PayloadRequest } from 'payload'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
import configPromise from '../../../../payload.config'
const payloadToken = 'payload-token'
export async function GET(
req: {
cookies: {
get: (name: string) => {
value: string
}
}
} & Request,
): Promise<Response> {
const payload = await getPayload({ config: configPromise })
const token = req.cookies.get(payloadToken)?.value
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
const collection = searchParams.get('collection') as CollectionSlug
const slug = searchParams.get('slug')
const previewSecret = searchParams.get('previewSecret')
if (previewSecret) {
return new Response('You are not allowed to preview this page', { status: 403 })
} else {
if (!path) {
return new Response('No path provided', { status: 404 })
}
if (!collection) {
return new Response('No path provided', { status: 404 })
}
if (!slug) {
return new Response('No path provided', { status: 404 })
}
if (!path.startsWith('/')) {
return new Response('This endpoint can only be used for internal previews', { status: 500 })
}
let user
try {
user = await payload.auth({
req: req as unknown as PayloadRequest,
headers: req.headers,
})
} catch (error) {
payload.logger.error({ err: error }, 'Error verifying token for live preview')
return new Response('You are not allowed to preview this page', { status: 403 })
}
const draft = await draftMode()
// You can add additional checks here to see if the user is allowed to preview this page
if (!user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
// Verify the given slug exists
try {
const docs = await payload.find({
collection,
draft: true,
where: {
slug: {
equals: slug,
},
},
})
if (!docs.docs.length) {
return new Response('Document not found', { status: 404 })
}
} catch (error) {
payload.logger.error({
err: error,
msg: 'Error verifying token for live preview:',
})
}
draft.enable()
redirect(path)
}
}

View File

@@ -0,0 +1,63 @@
import type { CollectionSlug, PayloadRequest } from 'payload'
import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import configPromise from '@payload-config'
export async function GET(
req: {
cookies: {
get: (name: string) => {
value: string
}
}
} & Request,
): Promise<Response> {
const payload = await getPayload({ config: configPromise })
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
const collection = searchParams.get('collection') as CollectionSlug
const slug = searchParams.get('slug')
const previewSecret = searchParams.get('previewSecret')
if (previewSecret !== process.env.PREVIEW_SECRET) {
return new Response('You are not allowed to preview this page', { status: 403 })
}
if (!path || !collection || !slug) {
return new Response('Insufficient search params', { status: 404 })
}
if (!path.startsWith('/')) {
return new Response('This endpoint can only be used for relative previews', { status: 500 })
}
let user
try {
user = await payload.auth({
req: req as unknown as PayloadRequest,
headers: req.headers,
})
} catch (error) {
payload.logger.error({ err: error }, 'Error verifying token for live preview')
return new Response('You are not allowed to preview this page', { status: 403 })
}
const draft = await draftMode()
if (!user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
// You can add additional checks here to see if the user is allowed to preview this page
draft.enable()
redirect(path)
}

View File

@@ -2,7 +2,7 @@ import type { CollectionAfterChangeHook } from 'payload'
import { revalidatePath } from 'next/cache'
import type { Page } from '../../../payload-types'
import type { Page } from '@payload-types'
export const revalidatePage: CollectionAfterChangeHook<Page> = ({ doc, previousDoc, req }) => {
if (req.context.skipRevalidate) {

View File

@@ -1,7 +1,6 @@
import type { CollectionConfig } from 'payload'
import type { CollectionConfig, CollectionSlug } from 'payload'
import richText from '../../fields/richText'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { loggedIn } from './access/loggedIn'
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
import { formatSlug } from './hooks/formatSlug'
@@ -17,12 +16,15 @@ export const Pages: CollectionConfig = {
},
admin: {
defaultColumns: ['title', 'slug', 'updatedAt'],
preview: (doc) => {
const path = generatePreviewPath({
slug: typeof doc?.slug === 'string' ? doc.slug : '',
collection: 'pages',
preview: ({ slug, collection }: { slug: string; collection: CollectionSlug }) => {
const encodedParams = new URLSearchParams({
slug,
collection,
path: `/${slug}`,
previewSecret: process.env.PREVIEW_SECRET || '',
})
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
return `${process.env.NEXT_PUBLIC_SERVER_URL}/preview?${encodedParams.toString()}`
},
useAsTitle: 'title',
},

View File

@@ -1,7 +1,7 @@
import Link from 'next/link'
import React from 'react'
import type { Page } from '../../payload-types'
import type { Page } from '@payload-types'
import { Button } from '../Button'

View File

@@ -1,16 +1,22 @@
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import type { MainMenu } from '../../payload-types'
import type { MainMenu } from '@payload-types'
import { getCachedGlobal } from '../../utilities/getGlobals'
import { CMSLink } from '../CMSLink'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
export async function Header() {
const header: MainMenu = await getCachedGlobal('main-menu', 1)()
const payload = await getPayload({ config: configPromise })
const header: MainMenu = await payload.findGlobal({
slug: 'main-menu',
depth: 1,
})
const navItems = header?.navItems || []

View File

@@ -1,4 +1,4 @@
import type { Page } from '../payload-types'
import type { Page } from '@payload-types'
// Used for pre-seeded content so that the homepage is not empty
// @ts-expect-error: Page type is not fully compatible with the provided object structure

View File

@@ -1,4 +1,4 @@
import type { Page } from '../payload-types'
import type { Page } from '@payload-types'
export const examplePage: Partial<Page> = {
slug: 'example-page',

View File

@@ -1,4 +1,4 @@
import type { Page } from '../payload-types'
import type { Page } from '@payload-types'
export const examplePageDraft: Partial<Page> = {
richText: [

View File

@@ -1,28 +0,0 @@
import type { CollectionSlug } from 'payload'
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
pages: '',
}
type Props = {
collection: keyof typeof collectionPrefixMap
slug: string
}
export const generatePreviewPath = ({ slug, collection }: Props) => {
const path = `${collectionPrefixMap[collection]}/${slug}`
const params = {
slug,
collection,
path,
}
const encodedParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
encodedParams.append(key, value)
})
return `/next/preview?${encodedParams.toString()}`
}

View File

@@ -1,27 +0,0 @@
import type { Config } from 'src/payload-types'
import { unstable_cache } from 'next/cache'
import { getPayload } from 'payload'
import configPromise from '../payload.config'
type Global = keyof Config['globals']
async function getGlobal(slug: Global, depth = 0) {
const payload = await getPayload({ config: configPromise })
const global = await payload.findGlobal({
slug,
depth,
})
return global
}
/**
* Returns a unstable_cache function mapped with the cache tag for the slug
*/
export const getCachedGlobal = (slug: Global, depth = 0) =>
unstable_cache(async () => getGlobal(slug, depth), [slug], {
tags: [`global_${slug}`],
})

View File

@@ -30,6 +30,9 @@
"@payload-config": [
"./src/payload.config.ts"
],
"@payload-types": [
"./src/payload-types.ts"
],
"react": [
"./node_modules/@types/react"
],

View File

@@ -9,8 +9,8 @@
h4,
h5,
h6 {
font-size: auto;
font-weight: auto;
font-size: unset;
font-weight: unset;
}
:root {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.19.0",
"version": "3.20.0",
"private": true,
"type": "module",
"scripts": {
@@ -126,7 +126,7 @@
"@sentry/nextjs": "^8.33.1",
"@sentry/node": "^8.33.1",
"@swc-node/register": "1.10.9",
"@swc/cli": "0.5.1",
"@swc/cli": "0.6.0",
"@swc/jest": "0.2.37",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
@@ -166,7 +166,7 @@
"shelljs": "0.8.5",
"slash": "3.0.0",
"sort-package-json": "^2.10.0",
"swc-plugin-transform-remove-imports": "2.0.0",
"swc-plugin-transform-remove-imports": "3.1.0",
"tempy": "1.0.1",
"tstyche": "^3.1.1",
"tsx": "4.19.2",

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.19.0",
"version": "3.20.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -60,7 +60,7 @@
"dependencies": {
"@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0",
"@swc/core": "1.7.10",
"@swc/core": "1.10.12",
"arg": "^5.0.0",
"chalk": "^4.1.0",
"comment-json": "^4.2.3",

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.19.0",
"version": "3.20.0",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -88,7 +88,7 @@
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
"@payloadcms/eslint-config": "workspace:*",
"@types/to-snake-case": "1.0.0",
"esbuild": "0.24.0",
"esbuild": "0.24.2",
"payload": "workspace:*"
},
"peerDependencies": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.19.0",
"version": "3.20.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -89,7 +89,7 @@
"@payloadcms/eslint-config": "workspace:*",
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"esbuild": "0.24.0",
"esbuild": "0.24.2",
"payload": "workspace:*"
},
"peerDependencies": {

View File

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

View File

@@ -88,6 +88,10 @@ export const traverseFields = ({
texts,
withinArrayOrBlockLocale,
}: Args) => {
if (row._uuid) {
data._uuid = row._uuid
}
fields.forEach((field) => {
let columnName = ''
let fieldName = ''

View File

@@ -20,7 +20,10 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
db,
fields,
ignoreResult,
joinQuery,
// TODO:
// When we support joins for write operations (create/update) - pass collectionSlug to the buildFindManyArgs
// Make a new argument in upsertRow.ts and pass the slug from every operation.
joinQuery: _joinQuery,
operation,
path = '',
req,
@@ -263,6 +266,9 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
}
}
// When versions are enabled, this is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
const arraysBlocksUUIDMap: Record<string, number | string> = {}
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
insertedBlockRows[blockName] = await adapter.insert({
@@ -273,6 +279,12 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
insertedBlockRows[blockName].forEach((row, i) => {
blockRows[i].row = row
if (
typeof row._uuid === 'string' &&
(typeof row.id === 'string' || typeof row.id === 'number')
) {
arraysBlocksUUIDMap[row._uuid] = row.id
}
})
const blockLocaleIndexMap: number[] = []
@@ -305,6 +317,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
arrays: blockRows.map(({ arrays }) => arrays),
db,
parentRows: insertedBlockRows[blockName],
uuidMap: arraysBlocksUUIDMap,
})
}
@@ -328,6 +341,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
arrays: [rowToInsert.arrays],
db,
parentRows: [insertedRow],
uuidMap: arraysBlocksUUIDMap,
})
// //////////////////////////////////
@@ -344,6 +358,14 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
})
}
if (Object.keys(arraysBlocksUUIDMap).length > 0) {
tableRows.forEach((row: any) => {
if (row.parent in arraysBlocksUUIDMap) {
row.parent = arraysBlocksUUIDMap[row.parent]
}
})
}
if (tableRows.length) {
await adapter.insert({
db,
@@ -414,13 +436,11 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
// RETRIEVE NEWLY UPDATED ROW
// //////////////////////////////////
joinQuery = operation === 'create' ? false : joinQuery
const findManyArgs = buildFindManyArgs({
adapter,
depth: 0,
fields,
joinQuery,
joinQuery: false,
select,
tableName,
})
@@ -438,7 +458,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
config: adapter.payload.config,
data: doc,
fields,
joinQuery,
joinQuery: false,
})
return result

View File

@@ -8,6 +8,7 @@ type Args = {
}[]
db: DrizzleAdapter['drizzle'] | DrizzleTransaction
parentRows: Record<string, unknown>[]
uuidMap?: Record<string, number | string>
}
type RowsByTable = {
@@ -20,7 +21,13 @@ type RowsByTable = {
}
}
export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): Promise<void> => {
export const insertArrays = async ({
adapter,
arrays,
db,
parentRows,
uuidMap = {},
}: Args): Promise<void> => {
// Maintain a map of flattened rows by table
const rowsByTable: RowsByTable = {}
@@ -74,6 +81,15 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
tableName,
values: row.rows,
})
insertedRows.forEach((row) => {
if (
typeof row._uuid === 'string' &&
(typeof row.id === 'string' || typeof row.id === 'number')
) {
uuidMap[row._uuid] = row.id
}
})
}
// Insert locale rows

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.19.0",
"version": "3.20.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -87,22 +87,23 @@
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"busboy": "^1.6.0",
"dequal": "2.0.3",
"file-type": "19.3.0",
"graphql-http": "^1.22.0",
"graphql-playground-html": "1.6.30",
"http-status": "2.1.0",
"path-to-regexp": "6.3.0",
"qs-esm": "7.0.2",
"react-diff-viewer-continued": "3.2.6",
"react-diff-viewer-continued": "4.0.4",
"sass": "1.77.4",
"sonner": "^1.7.0",
"uuid": "10.0.0"
},
"devDependencies": {
"@babel/cli": "7.25.9",
"@babel/core": "7.26.0",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.25.9",
"@babel/cli": "7.26.4",
"@babel/core": "7.26.7",
"@babel/preset-env": "7.26.7",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@next/eslint-plugin-next": "15.1.5",
"@payloadcms/eslint-config": "workspace:*",
@@ -110,12 +111,12 @@
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
"esbuild": "0.24.0",
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"esbuild": "0.24.2",
"esbuild-sass-plugin": "3.3.1",
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
"eslint-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"payload": "workspace:*",
"swc-plugin-transform-remove-imports": "2.0.0"
"swc-plugin-transform-remove-imports": "3.1.0"
},
"peerDependencies": {
"graphql": "^16.8.1",

View File

@@ -0,0 +1,13 @@
'use client'
import React, { createContext } from 'react'
type SelectedLocalesContextType = {
selectedLocales: string[]
}
export const SelectedLocalesContext = createContext<SelectedLocalesContextType>({
selectedLocales: [],
})
export const useSelectedLocales = () => React.useContext(SelectedLocalesContext)

View File

@@ -46,6 +46,10 @@
margin: 0 0 0 var(--base);
}
&__modifiedCheckBox {
margin: 0 0 0 var(--base);
}
@include mid-break {
&__intro,
&__header {
@@ -57,6 +61,7 @@
gap: calc(var(--base) / 4);
}
&__restore {
margin: calc(var(--base) * 0.5) 0 0 0;
}

View File

@@ -1,33 +1,45 @@
'use client'
import type { OptionObject } from 'payload'
import { Gutter, useConfig, useDocumentInfo, usePayloadAPI, useTranslation } from '@payloadcms/ui'
import { CheckboxInput, Gutter, useConfig, useDocumentInfo, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import React, { useState } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation.js'
import React, { useEffect, useMemo, useState } from 'react'
import type { CompareOption, DefaultVersionsViewProps } from './types.js'
import { diffComponents } from '../RenderFieldsToDiff/fields/index.js'
import { RenderFieldsToDiff } from '../RenderFieldsToDiff/index.js'
import Restore from '../Restore/index.js'
import { SelectComparison } from '../SelectComparison/index.js'
import { SelectLocales } from '../SelectLocales/index.js'
import './index.scss'
import { SelectLocales } from '../SelectLocales/index.js'
import { SelectedLocalesContext } from './SelectedLocalesContext.js'
import { SetStepNav } from './SetStepNav.js'
const baseClass = 'view-version'
export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
canUpdate,
doc,
docPermissions,
initialComparisonDoc,
latestDraftVersion,
latestPublishedVersion,
localeOptions,
modifiedOnly: modifiedOnlyProp,
RenderedDiff,
selectedLocales: selectedLocalesProp,
versionID,
}) => {
const { config, getEntityConfig } = useConfig()
const availableLocales = useMemo(
() =>
config.localization
? config.localization.locales.map((locale) => ({
label: locale.label,
value: locale.code,
}))
: [],
[config.localization],
)
const { i18n } = useTranslation()
const { id, collectionSlug, globalSlug } = useDocumentInfo()
@@ -35,9 +47,43 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
const [globalConfig] = useState(() => getEntityConfig({ globalSlug }))
const [locales, setLocales] = useState<OptionObject[]>(localeOptions)
const [selectedLocales, setSelectedLocales] = useState<OptionObject[]>(selectedLocalesProp)
const [compareValue, setCompareValue] = useState<CompareOption>()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [modifiedOnly, setModifiedOnly] = useState(modifiedOnlyProp)
function onToggleModifiedOnly() {
setModifiedOnly(!modifiedOnly)
}
useEffect(() => {
// If the selected comparison doc or locales change, update URL params so that version page RSC
// can update the version comparison state
const current = new URLSearchParams(Array.from(searchParams.entries()))
if (!compareValue) {
current.delete('compareValue')
} else {
current.set('compareValue', compareValue?.value)
}
if (!selectedLocales) {
current.delete('localeCodes')
} else {
current.set('localeCodes', JSON.stringify(selectedLocales.map((locale) => locale.value)))
}
if (!modifiedOnly) {
current.delete('modifiedOnly')
} else {
current.set('modifiedOnly', 'true')
}
const search = current.toString()
const query = search ? `?${search}` : ''
router.push(`${pathname}${query}`)
}, [compareValue, pathname, router, searchParams, selectedLocales, modifiedOnly])
const {
admin: { dateFormat },
@@ -54,19 +100,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
collectionSlug || globalSlug
}/versions`
const compareFetchURL = compareValue?.value && `${compareBaseURL}/${compareValue.value}`
const [{ data: currentComparisonDoc }] = usePayloadAPI(compareFetchURL, {
initialData: initialComparisonDoc,
initialParams: { depth: 1, draft: 'true', locale: 'all' },
})
const comparison = compareValue?.value && currentComparisonDoc?.version // the `version` key is only present on `versions` documents
const canUpdate = docPermissions?.update
const localeValues = locales && locales.map((locale) => locale.value)
const draftsEnabled = Boolean((collectionConfig || globalConfig)?.versions.drafts)
return (
@@ -101,6 +134,14 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
versionID={versionID}
/>
)}
<span className={`${baseClass}__modifiedCheckBox`}>
<CheckboxInput
checked={modifiedOnly}
id={'modifiedOnly'}
label={i18n.t('version:modifiedOnly')}
onToggle={onToggleModifiedOnly}
/>
</span>
</header>
</div>
<div className={`${baseClass}__controls`}>
@@ -115,28 +156,18 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
versionID={versionID}
/>
{localization && (
<SelectLocales onChange={setLocales} options={localeOptions} value={locales} />
<SelectLocales
onChange={setSelectedLocales}
options={availableLocales}
value={selectedLocales}
/>
)}
</div>
{doc?.version && (
<RenderFieldsToDiff
comparison={comparison}
diffComponents={diffComponents}
fieldPermissions={docPermissions?.fields}
fields={(collectionConfig || globalConfig)?.fields}
i18n={i18n}
locales={localeValues}
version={
globalConfig
? {
...doc?.version,
createdAt: doc?.version?.createdAt || doc.createdAt,
updatedAt: doc?.version?.updatedAt || doc.updatedAt,
}
: doc?.version
}
/>
)}
<SelectedLocalesContext.Provider
value={{ selectedLocales: selectedLocales.map((locale) => locale.value) }}
>
{doc?.version && RenderedDiff}
</SelectedLocalesContext.Provider>
</Gutter>
</main>
)

View File

@@ -1,9 +1,4 @@
import type {
Document,
OptionObject,
SanitizedCollectionPermission,
SanitizedGlobalPermission,
} from 'payload'
import type { Document, OptionObject } from 'payload'
export type CompareOption = {
label: React.ReactNode | string
@@ -13,11 +8,12 @@ export type CompareOption = {
}
export type DefaultVersionsViewProps = {
readonly canUpdate: boolean
readonly doc: Document
readonly docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
readonly initialComparisonDoc: Document
readonly latestDraftVersion?: string
readonly latestPublishedVersion?: string
readonly localeOptions: OptionObject[]
modifiedOnly: boolean
readonly RenderedDiff: React.ReactNode
readonly selectedLocales: OptionObject[]
readonly versionID?: string
}

View File

@@ -0,0 +1,67 @@
'use client'
const baseClass = 'render-field-diffs'
import type { VersionField } from 'payload'
import './index.scss'
import { ShimmerEffect } from '@payloadcms/ui'
import React, { Fragment, useEffect } from 'react'
export const RenderVersionFieldsToDiff = ({
versionFields,
}: {
versionFields: VersionField[]
}): React.ReactNode => {
const [hasMounted, setHasMounted] = React.useState(false)
// defer rendering until after the first mount as the CSS is loaded with Emotion
// this will ensure that the CSS is loaded before rendering the diffs and prevent CLS
useEffect(() => {
setHasMounted(true)
}, [])
return (
<div className={baseClass}>
{!hasMounted ? (
<Fragment>
<ShimmerEffect height="8rem" width="100%" />
</Fragment>
) : (
versionFields?.map((field, fieldIndex) => {
if (field.fieldByLocale) {
const LocaleComponents: React.ReactNode[] = []
for (const [locale, baseField] of Object.entries(field.fieldByLocale)) {
LocaleComponents.push(
<div
className={`${baseClass}__locale`}
data-field-path={baseField.path}
data-locale={locale}
key={[locale, fieldIndex].join('-')}
>
<div className={`${baseClass}__locale-value`}>{baseField.CustomComponent}</div>
</div>,
)
}
return (
<div className={`${baseClass}__field`} key={fieldIndex}>
{LocaleComponents}
</div>
)
} else if (field.field) {
return (
<div
className={`${baseClass}__field field__${field.field.type}`}
data-field-path={field.field.path}
key={fieldIndex}
>
{field.field.CustomComponent}
</div>
)
}
return null
})
)}
</div>
)
}

View File

@@ -0,0 +1,413 @@
import type { I18nClient } from '@payloadcms/translations'
import type {
BaseVersionField,
ClientField,
ClientFieldSchemaMap,
Field,
FieldDiffClientProps,
FieldDiffServerProps,
FieldTypes,
PayloadComponent,
PayloadRequest,
SanitizedFieldPermissions,
VersionField,
} from 'payload'
import type { DiffMethod } from 'react-diff-viewer-continued'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { dequal } from 'dequal/lite'
import { fieldIsID, getUniqueListBy, tabHasName } from 'payload/shared'
import { diffMethods } from './fields/diffMethods.js'
import { diffComponents } from './fields/index.js'
import { getFieldPathsModified } from './utilities/getFieldPathsModified.js'
export type BuildVersionFieldsArgs = {
clientSchemaMap: ClientFieldSchemaMap
comparisonSiblingData: object
customDiffComponents: Partial<
Record<FieldTypes, PayloadComponent<FieldDiffServerProps, FieldDiffClientProps>>
>
entitySlug: string
fieldPermissions:
| {
[key: string]: SanitizedFieldPermissions
}
| true
fields: Field[]
i18n: I18nClient
modifiedOnly: boolean
parentIndexPath: string
parentPath: string
parentSchemaPath: string
req: PayloadRequest
selectedLocales: string[]
versionSiblingData: object
}
/**
* Build up an object that contains rendered diff components for each field.
* This is then sent to the client to be rendered.
*
* Here, the server is responsible for traversing through the document data and building up this
* version state object.
*/
export const buildVersionFields = ({
clientSchemaMap,
comparisonSiblingData,
customDiffComponents,
entitySlug,
fieldPermissions,
fields,
i18n,
modifiedOnly,
parentIndexPath,
parentPath,
parentSchemaPath,
req,
selectedLocales,
versionSiblingData,
}: BuildVersionFieldsArgs): {
versionFields: VersionField[]
} => {
const versionFields: VersionField[] = []
let fieldIndex = -1
for (const field of fields) {
fieldIndex++
if (fieldIsID(field)) {
continue
}
const { indexPath, path, schemaPath } = getFieldPathsModified({
field,
index: fieldIndex,
parentIndexPath: 'name' in field ? '' : parentIndexPath,
parentPath,
parentSchemaPath,
})
const clientField = clientSchemaMap.get(entitySlug + '.' + schemaPath)
if (!clientField) {
req.payload.logger.error({
clientFieldKey: entitySlug + '.' + schemaPath,
clientSchemaMapKeys: Array.from(clientSchemaMap.keys()),
msg: 'No client field found for ' + entitySlug + '.' + schemaPath,
parentPath,
parentSchemaPath,
path,
schemaPath,
})
throw new Error('No client field found for ' + entitySlug + '.' + schemaPath)
}
const versionField: VersionField = {}
const isLocalized = 'localized' in field && field.localized
const fieldName: null | string = 'name' in field ? field.name : null
const versionValue = fieldName ? versionSiblingData?.[fieldName] : versionSiblingData
const comparisonValue = fieldName ? comparisonSiblingData?.[fieldName] : comparisonSiblingData
if (isLocalized) {
versionField.fieldByLocale = {}
for (const locale of selectedLocales) {
versionField.fieldByLocale[locale] = buildVersionField({
clientField: clientField as ClientField,
clientSchemaMap,
comparisonValue: comparisonValue?.[locale],
customDiffComponents,
entitySlug,
field,
fieldPermissions,
i18n,
indexPath,
locale,
modifiedOnly,
parentPath,
parentSchemaPath,
path,
req,
schemaPath,
selectedLocales,
versionValue: versionValue?.[locale],
})
if (!versionField.fieldByLocale[locale]) {
continue
}
}
} else {
versionField.field = buildVersionField({
clientField: clientField as ClientField,
clientSchemaMap,
comparisonValue,
customDiffComponents,
entitySlug,
field,
fieldPermissions,
i18n,
indexPath,
modifiedOnly,
parentPath,
parentSchemaPath,
path,
req,
schemaPath,
selectedLocales,
versionValue,
})
if (!versionField.field) {
continue
}
}
versionFields.push(versionField)
}
return {
versionFields,
}
}
const buildVersionField = ({
clientField,
clientSchemaMap,
comparisonValue,
customDiffComponents,
entitySlug,
field,
fieldPermissions,
i18n,
indexPath,
locale,
modifiedOnly,
parentPath,
parentSchemaPath,
path,
req,
schemaPath,
selectedLocales,
versionValue,
}: {
clientField: ClientField
comparisonValue: unknown
field: Field
indexPath: string
locale?: string
modifiedOnly?: boolean
path: string
schemaPath: string
versionValue: unknown
} & Omit<
BuildVersionFieldsArgs,
'comparisonSiblingData' | 'fields' | 'parentIndexPath' | 'versionSiblingData'
>): BaseVersionField | null => {
const fieldName: null | string = 'name' in field ? field.name : null
const diffMethod: DiffMethod = diffMethods[field.type] || 'CHARS'
const hasPermission =
fieldPermissions === true ||
!fieldName ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.read
const subFieldPermissions =
fieldPermissions === true ||
!fieldName ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.fields
if (!hasPermission) {
return null
}
if (modifiedOnly && dequal(versionValue, comparisonValue)) {
return null
}
const CustomComponent = field?.admin?.components?.Diff ?? customDiffComponents?.[field.type]
const DefaultComponent = diffComponents?.[field.type]
const baseVersionField: BaseVersionField = {
type: field.type,
fields: [],
path,
schemaPath,
}
if (field.type === 'tabs' && 'tabs' in field) {
baseVersionField.tabs = []
let tabIndex = -1
for (const tab of field.tabs) {
tabIndex++
const isNamedTab = tabHasName(tab)
const {
indexPath: tabIndexPath,
path: tabPath,
schemaPath: tabSchemaPath,
} = getFieldPathsModified({
field: {
...tab,
type: 'tab',
},
index: tabIndex,
parentIndexPath: indexPath,
parentPath,
parentSchemaPath,
})
baseVersionField.tabs.push({
name: 'name' in tab ? tab.name : null,
fields: buildVersionFields({
clientSchemaMap,
comparisonSiblingData: 'name' in tab ? comparisonValue?.[tab.name] : comparisonValue,
customDiffComponents,
entitySlug,
fieldPermissions,
fields: tab.fields,
i18n,
modifiedOnly,
parentIndexPath: isNamedTab ? '' : tabIndexPath,
parentPath: tabPath,
parentSchemaPath: tabSchemaPath,
req,
selectedLocales,
versionSiblingData: 'name' in tab ? versionValue?.[tab.name] : versionValue,
}).versionFields,
label: tab.label,
})
}
} // At this point, we are dealing with a `row`, etc
else if ('fields' in field) {
if (field.type === 'array' && versionValue) {
const arrayValue = Array.isArray(versionValue) ? versionValue : []
baseVersionField.rows = []
for (let i = 0; i < arrayValue.length; i++) {
const comparisonRow = comparisonValue?.[i] || {}
const versionRow = arrayValue?.[i] || {}
baseVersionField.rows[i] = buildVersionFields({
clientSchemaMap,
comparisonSiblingData: comparisonRow,
customDiffComponents,
entitySlug,
fieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
parentIndexPath: 'name' in field ? '' : indexPath,
parentPath: path + '.' + i,
parentSchemaPath: schemaPath,
req,
selectedLocales,
versionSiblingData: versionRow,
}).versionFields
}
} else {
baseVersionField.fields = buildVersionFields({
clientSchemaMap,
comparisonSiblingData: comparisonValue as object,
customDiffComponents,
entitySlug,
fieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
parentIndexPath: 'name' in field ? '' : indexPath,
parentPath: path,
parentSchemaPath: schemaPath,
req,
selectedLocales,
versionSiblingData: versionValue as object,
}).versionFields
}
} else if (field.type === 'blocks') {
baseVersionField.rows = []
const blocksValue = Array.isArray(versionValue) ? versionValue : []
for (let i = 0; i < blocksValue.length; i++) {
const comparisonRow = comparisonValue?.[i] || {}
const versionRow = blocksValue[i] || {}
const versionBlock = field.blocks.find((block) => block.slug === versionRow.blockType)
let fields = []
if (versionRow.blockType === comparisonRow.blockType) {
fields = versionBlock.fields
} else {
const comparisonBlock = field.blocks.find((block) => block.slug === comparisonRow.blockType)
if (comparisonBlock) {
fields = getUniqueListBy<Field>(
[...versionBlock.fields, ...comparisonBlock.fields],
'name',
)
} else {
fields = versionBlock.fields
}
}
baseVersionField.rows[i] = buildVersionFields({
clientSchemaMap,
comparisonSiblingData: comparisonRow,
customDiffComponents,
entitySlug,
fieldPermissions,
fields,
i18n,
modifiedOnly,
parentIndexPath: 'name' in field ? '' : indexPath,
parentPath: path + '.' + i,
parentSchemaPath: schemaPath + '.' + versionBlock.slug,
req,
selectedLocales,
versionSiblingData: versionRow,
}).versionFields
}
}
const clientCellProps: FieldDiffClientProps = {
baseVersionField: {
...baseVersionField,
CustomComponent: undefined,
},
comparisonValue,
diffMethod,
field: clientField,
fieldPermissions: subFieldPermissions,
versionValue,
}
const serverCellProps: FieldDiffServerProps = {
...clientCellProps,
clientField,
field,
i18n,
req,
selectedLocales,
}
baseVersionField.CustomComponent = RenderServerComponent({
clientProps: locale
? ({
...clientCellProps,
locale,
} as FieldDiffClientProps)
: clientCellProps,
Component: CustomComponent,
Fallback: DefaultComponent,
importMap: req.payload.importMap,
key: 'diff component',
serverProps: locale
? ({
...serverCellProps,
locale,
} as FieldDiffServerProps)
: serverCellProps,
})
return baseVersionField
}

View File

@@ -1,46 +1,43 @@
'use client'
import type { CollapsibleFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui'
import React from 'react'
import type { DiffComponentProps } from '../types.js'
import { useSelectedLocales } from '../../../Default/SelectedLocalesContext.js'
import { DiffCollapser } from '../../DiffCollapser/index.js'
import { RenderFieldsToDiff } from '../../index.js'
import { RenderVersionFieldsToDiff } from '../../RenderVersionFieldsToDiff.js'
const baseClass = 'collapsible-diff'
export const Collapsible: React.FC<DiffComponentProps> = ({
comparison,
diffComponents,
export const Collapsible: CollapsibleFieldDiffClientComponent = ({
baseVersionField,
comparisonValue,
field,
fieldPermissions,
fields,
i18n,
locales,
version,
versionValue,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
if (!baseVersionField.fields?.length) {
return null
}
return (
<div className={baseClass}>
<DiffCollapser
comparison={comparison}
fields={fields}
comparison={comparisonValue}
fields={field.fields}
label={
'label' in field &&
field.label &&
typeof field.label !== 'function' && <span>{getTranslation(field.label, i18n)}</span>
}
locales={locales}
version={version}
locales={selectedLocales}
version={versionValue}
>
<RenderFieldsToDiff
comparison={comparison}
diffComponents={diffComponents}
fieldPermissions={fieldPermissions}
fields={fields}
i18n={i18n}
locales={locales}
version={version}
/>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />
</DiffCollapser>
</div>
)

View File

@@ -1,32 +1,34 @@
'use client'
import type { GroupFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import './index.scss'
import type { DiffComponentProps } from '../types.js'
import { useTranslation } from '@payloadcms/ui'
import React from 'react'
import { useSelectedLocales } from '../../../Default/SelectedLocalesContext.js'
import { DiffCollapser } from '../../DiffCollapser/index.js'
import { RenderFieldsToDiff } from '../../index.js'
import { RenderVersionFieldsToDiff } from '../../RenderVersionFieldsToDiff.js'
const baseClass = 'group-diff'
export const Group: React.FC<DiffComponentProps> = ({
comparison,
diffComponents,
export const Group: GroupFieldDiffClientComponent = ({
baseVersionField,
comparisonValue,
field,
fieldPermissions,
fields,
i18n,
locale,
locales,
version,
versionValue,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
return (
<div className={baseClass}>
<DiffCollapser
comparison={comparison}
fields={fields}
comparison={comparisonValue}
fields={field.fields}
label={
'label' in field &&
field.label &&
@@ -37,18 +39,10 @@ export const Group: React.FC<DiffComponentProps> = ({
</span>
)
}
locales={locales}
version={version}
locales={selectedLocales}
version={versionValue}
>
<RenderFieldsToDiff
comparison={comparison}
diffComponents={diffComponents}
fieldPermissions={fieldPermissions}
fields={fields}
i18n={i18n}
locales={locales}
version={version}
/>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />
</DiffCollapser>
</div>
)

View File

@@ -1,31 +1,34 @@
'use client'
import type { ClientField } from 'payload'
import type { FieldDiffClientProps } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui'
import './index.scss'
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
import React from 'react'
import type { DiffComponentProps } from '../types.js'
import { useSelectedLocales } from '../../../Default/SelectedLocalesContext.js'
import { DiffCollapser } from '../../DiffCollapser/index.js'
import './index.scss'
import { RenderFieldsToDiff } from '../../index.js'
import { RenderVersionFieldsToDiff } from '../../RenderVersionFieldsToDiff.js'
import { getFieldsForRowComparison } from '../../utilities/getFieldsForRowComparison.js'
const baseClass = 'iterable-diff'
export const Iterable: React.FC<DiffComponentProps> = ({
comparison,
diffComponents,
export const Iterable: React.FC<FieldDiffClientProps> = ({
baseVersionField,
comparisonValue,
field,
fieldPermissions,
i18n,
locale,
locales,
version,
versionValue,
}) => {
const versionRowCount = Array.isArray(version) ? version.length : 0
const comparisonRowCount = Array.isArray(comparison) ? comparison.length : 0
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
const versionRowCount = Array.isArray(versionValue) ? versionValue.length : 0
const comparisonRowCount = Array.isArray(comparisonValue) ? comparisonValue.length : 0
const maxRows = Math.max(versionRowCount, comparisonRowCount)
if (!fieldIsArrayType(field) && !fieldIsBlockType(field)) {
@@ -35,7 +38,7 @@ export const Iterable: React.FC<DiffComponentProps> = ({
return (
<div className={baseClass}>
<DiffCollapser
comparison={comparison}
comparison={comparisonValue}
field={field}
isIterable
label={
@@ -48,18 +51,20 @@ export const Iterable: React.FC<DiffComponentProps> = ({
</span>
)
}
locales={locales}
version={version}
locales={selectedLocales}
version={versionValue}
>
{maxRows > 0 && (
<div className={`${baseClass}__rows`}>
{Array.from(Array(maxRows).keys()).map((row, i) => {
const versionRow = version?.[i] || {}
const comparisonRow = comparison?.[i] || {}
const versionRow = versionValue?.[i] || {}
const comparisonRow = comparisonValue?.[i] || {}
const fields: ClientField[] = getFieldsForRowComparison({
const { fields, versionFields } = getFieldsForRowComparison({
baseVersionField,
comparisonRow,
field,
row: i,
versionRow,
})
@@ -72,18 +77,10 @@ export const Iterable: React.FC<DiffComponentProps> = ({
comparison={comparisonRow}
fields={fields}
label={rowLabel}
locales={locales}
locales={selectedLocales}
version={versionRow}
>
<RenderFieldsToDiff
comparison={comparisonRow}
diffComponents={diffComponents}
fieldPermissions={fieldPermissions}
fields={fields}
i18n={i18n}
locales={locales}
version={versionRow}
/>
<RenderVersionFieldsToDiff versionFields={versionFields} />
</DiffCollapser>
</div>
)

View File

@@ -1,20 +1,19 @@
'use client'
import type { ClientCollectionConfig, ClientField, RelationshipFieldClient } from 'payload'
import type {
ClientCollectionConfig,
ClientField,
RelationshipFieldDiffClientComponent,
} from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useConfig } from '@payloadcms/ui'
import { useConfig, useTranslation } from '@payloadcms/ui'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
import React from 'react'
import ReactDiffViewerImport from 'react-diff-viewer-continued'
import type { DiffComponentProps } from '../types.js'
import ReactDiffViewer from 'react-diff-viewer-continued'
import Label from '../../Label/index.js'
import { diffStyles } from '../styles.js'
import './index.scss'
const ReactDiffViewer = (ReactDiffViewerImport.default ||
ReactDiffViewerImport) as unknown as typeof ReactDiffViewerImport.default
import { diffStyles } from '../styles.js'
const baseClass = 'relationship-diff'
@@ -99,13 +98,14 @@ const generateLabelFromValue = (
return valueToReturn
}
export const Relationship: React.FC<DiffComponentProps<RelationshipFieldClient>> = ({
comparison,
export const Relationship: RelationshipFieldDiffClientComponent = ({
comparisonValue,
field,
i18n,
locale,
version,
versionValue,
}) => {
const { i18n } = useTranslation()
const placeholder = `[${i18n.t('general:noValue')}]`
const {
@@ -115,25 +115,27 @@ export const Relationship: React.FC<DiffComponentProps<RelationshipFieldClient>>
let versionToRender: string | undefined = placeholder
let comparisonToRender: string | undefined = placeholder
if (version) {
if ('hasMany' in field && field.hasMany && Array.isArray(version)) {
if (versionValue) {
if ('hasMany' in field && field.hasMany && Array.isArray(versionValue)) {
versionToRender =
version.map((val) => generateLabelFromValue(collections, field, locale, val)).join(', ') ||
placeholder
versionValue
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ') || placeholder
} else {
versionToRender = generateLabelFromValue(collections, field, locale, version) || placeholder
versionToRender =
generateLabelFromValue(collections, field, locale, versionValue) || placeholder
}
}
if (comparison) {
if ('hasMany' in field && field.hasMany && Array.isArray(comparison)) {
if (comparisonValue) {
if ('hasMany' in field && field.hasMany && Array.isArray(comparisonValue)) {
comparisonToRender =
comparison
comparisonValue
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ') || placeholder
} else {
comparisonToRender =
generateLabelFromValue(collections, field, locale, comparison) || placeholder
generateLabelFromValue(collections, field, locale, comparisonValue) || placeholder
}
}

View File

@@ -1,38 +1,16 @@
'use client'
import { getTranslation } from '@payloadcms/translations'
import type { RowFieldDiffClientComponent } from 'payload'
import React from 'react'
import type { DiffComponentProps } from '../types.js'
import { RenderFieldsToDiff } from '../../index.js'
import Label from '../../Label/index.js'
import { RenderVersionFieldsToDiff } from '../../RenderVersionFieldsToDiff.js'
const baseClass = 'row-diff'
export const Row: React.FC<DiffComponentProps> = ({
comparison,
diffComponents,
field,
fieldPermissions,
fields,
i18n,
locales,
version,
}) => {
export const Row: RowFieldDiffClientComponent = ({ baseVersionField }) => {
return (
<div className={baseClass}>
{'label' in field && field.label && typeof field.label !== 'function' && (
<Label>{getTranslation(field.label, i18n)}</Label>
)}
<RenderFieldsToDiff
comparison={comparison}
diffComponents={diffComponents}
fieldPermissions={fieldPermissions}
fields={fields}
i18n={i18n}
locales={locales}
version={version}
/>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />
</div>
)
}

View File

@@ -1,9 +1,6 @@
'use client'
import React from 'react'
import ReactDiffViewerImport, { DiffMethod } from 'react-diff-viewer-continued'
const ReactDiffViewer = (ReactDiffViewerImport.default ||
ReactDiffViewerImport) as unknown as typeof ReactDiffViewerImport.default
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
export const DiffViewer: React.FC<{
comparisonToRender: string

View File

@@ -1,16 +1,15 @@
'use client'
import type { I18nClient } from '@payloadcms/translations'
import type { OptionObject, SelectField, SelectFieldClient } from 'payload'
import type { OptionObject, SelectField, SelectFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui'
import React from 'react'
import type { DiffComponentProps } from '../types.js'
import Label from '../../Label/index.js'
import './index.scss'
import { diffStyles } from '../styles.js'
import { DiffViewer } from './DiffViewer/index.js'
import './index.scss'
const baseClass = 'select-diff'
@@ -45,30 +44,45 @@ const getTranslatedOptions = (
return typeof options === 'string' ? options : getTranslation(options.label, i18n)
}
export const Select: React.FC<DiffComponentProps<SelectFieldClient>> = ({
comparison,
export const Select: SelectFieldDiffClientComponent = ({
comparisonValue,
diffMethod,
field,
i18n,
locale,
version,
versionValue,
}) => {
const { i18n } = useTranslation()
let placeholder = ''
if (version === comparison) {
if (versionValue == comparisonValue) {
placeholder = `[${i18n.t('general:noValue')}]`
}
const options = 'options' in field && field.options
const comparisonToRender =
typeof comparison !== 'undefined'
? getTranslatedOptions(getOptionsToRender(comparison, options, field.hasMany), i18n)
typeof comparisonValue !== 'undefined'
? getTranslatedOptions(
getOptionsToRender(
typeof comparisonValue === 'string' ? comparisonValue : JSON.stringify(comparisonValue),
options,
field.hasMany,
),
i18n,
)
: placeholder
const versionToRender =
typeof version !== 'undefined'
? getTranslatedOptions(getOptionsToRender(version, options, field.hasMany), i18n)
typeof versionValue !== 'undefined'
? getTranslatedOptions(
getOptionsToRender(
typeof versionValue === 'string' ? versionValue : JSON.stringify(versionValue),
options,
field.hasMany,
),
i18n,
)
: placeholder
return (

View File

@@ -1,37 +1,55 @@
'use client'
import type { ClientTab, TabsFieldClient } from 'payload'
import type {
ClientTab,
FieldDiffClientProps,
TabsFieldClient,
TabsFieldDiffClientComponent,
VersionTab,
} from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui'
import React from 'react'
import type { DiffComponentProps } from '../types.js'
import { DiffCollapser } from '../../DiffCollapser/index.js'
import { RenderFieldsToDiff } from '../../index.js'
import './index.scss'
import { useSelectedLocales } from '../../../Default/SelectedLocalesContext.js'
import { DiffCollapser } from '../../DiffCollapser/index.js'
import { RenderVersionFieldsToDiff } from '../../RenderVersionFieldsToDiff.js'
const baseClass = 'tabs-diff'
export const Tabs: React.FC<DiffComponentProps<TabsFieldClient>> = (props) => {
const { comparison, field, locales, version } = props
export const Tabs: TabsFieldDiffClientComponent = (props) => {
const { baseVersionField, comparisonValue, field, versionValue } = props
const { selectedLocales } = useSelectedLocales()
return (
<div className={baseClass}>
{field.tabs.map((tab, i) => {
{baseVersionField.tabs.map((tab, i) => {
if (!tab?.fields?.length) {
return null
}
const fieldTab = field.tabs?.[i]
return (
<div className={`${baseClass}__tab`} key={i}>
{(() => {
if ('name' in tab && locales && tab.localized) {
if ('name' in fieldTab && selectedLocales && fieldTab.localized) {
// Named localized tab
return locales.map((locale, index) => {
return selectedLocales.map((locale, index) => {
const localizedTabProps = {
...props,
comparison: comparison?.[tab.name]?.[locale],
version: version?.[tab.name]?.[locale],
comparison: comparisonValue?.[tab.name]?.[locale],
version: versionValue?.[tab.name]?.[locale],
}
return (
<div className={`${baseClass}__tab-locale`} key={[locale, index].join('-')}>
<div className={`${baseClass}__tab-locale-value`}>
<Tab key={locale} {...localizedTabProps} locale={locale} tab={tab} />
<Tab
key={locale}
{...localizedTabProps}
fieldTab={fieldTab}
locale={locale}
tab={tab}
/>
</div>
</div>
)
@@ -40,13 +58,13 @@ export const Tabs: React.FC<DiffComponentProps<TabsFieldClient>> = (props) => {
// Named tab
const namedTabProps = {
...props,
comparison: comparison?.[tab.name],
version: version?.[tab.name],
comparison: comparisonValue?.[tab.name],
version: versionValue?.[tab.name],
}
return <Tab key={i} {...namedTabProps} tab={tab} />
return <Tab fieldTab={fieldTab} key={i} {...namedTabProps} tab={tab} />
} else {
// Unnamed tab
return <Tab key={i} {...props} tab={tab} />
return <Tab fieldTab={fieldTab} key={i} {...props} tab={tab} />
}
})()}
</div>
@@ -57,23 +75,22 @@ export const Tabs: React.FC<DiffComponentProps<TabsFieldClient>> = (props) => {
}
type TabProps = {
tab: ClientTab
} & DiffComponentProps<TabsFieldClient>
fieldTab: ClientTab
tab: VersionTab
} & FieldDiffClientProps<TabsFieldClient>
const Tab: React.FC<TabProps> = ({ comparisonValue, fieldTab, locale, tab, versionValue }) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
if (!tab.fields?.length) {
return null
}
const Tab: React.FC<TabProps> = ({
comparison,
diffComponents,
fieldPermissions,
i18n,
locale,
locales,
tab,
version,
}) => {
return (
<DiffCollapser
comparison={comparison}
fields={tab.fields}
comparison={comparisonValue}
fields={fieldTab.fields}
label={
'label' in tab &&
tab.label &&
@@ -84,18 +101,10 @@ const Tab: React.FC<TabProps> = ({
</span>
)
}
locales={locales}
version={version}
locales={selectedLocales}
version={versionValue}
>
<RenderFieldsToDiff
comparison={comparison}
diffComponents={diffComponents}
fieldPermissions={fieldPermissions}
fields={tab.fields}
i18n={i18n}
locales={locales}
version={version}
/>
<RenderVersionFieldsToDiff versionFields={tab.fields} />
</DiffCollapser>
)
}

View File

@@ -1,9 +1,7 @@
'use client'
import React from 'react'
import ReactDiffViewerImport, { DiffMethod } from 'react-diff-viewer-continued'
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
const ReactDiffViewer = (ReactDiffViewerImport.default ||
ReactDiffViewerImport) as unknown as typeof ReactDiffViewerImport.default
export const DiffViewer: React.FC<{
comparisonToRender: string
diffMethod: string

View File

@@ -1,44 +1,36 @@
'use client'
import type { TextFieldClient } from 'payload'
import type { TextFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui'
import React from 'react'
import type { DiffComponentProps } from '../types.js'
import Label from '../../Label/index.js'
import './index.scss'
import { diffStyles } from '../styles.js'
import { DiffViewer } from './DiffViewer/index.js'
import './index.scss'
const baseClass = 'text-diff'
export const Text: React.FC<DiffComponentProps<TextFieldClient>> = ({
comparison,
export const Text: TextFieldDiffClientComponent = ({
comparisonValue,
diffMethod,
field,
i18n,
isRichText = false,
locale,
version,
versionValue,
}) => {
const { i18n } = useTranslation()
let placeholder = ''
if (version === comparison) {
if (versionValue == comparisonValue) {
placeholder = `[${i18n.t('general:noValue')}]`
}
let versionToRender = version
let comparisonToRender = comparison
if (isRichText) {
if (typeof version === 'object') {
versionToRender = JSON.stringify(version, null, 2)
}
if (typeof comparison === 'object') {
comparisonToRender = JSON.stringify(comparison, null, 2)
}
}
const versionToRender: string =
typeof versionValue === 'string' ? versionValue : JSON.stringify(versionValue, null, 2)
const comparisonToRender =
typeof comparisonValue === 'string' ? comparisonValue : JSON.stringify(comparisonValue, null, 2)
return (
<div className={baseClass}>

View File

@@ -1,3 +1,5 @@
import type { FieldDiffClientProps, FieldTypes } from 'payload'
import { Collapsible } from './Collapsible/index.js'
import { Group } from './Group/index.js'
import { Iterable } from './Iterable/index.js'
@@ -7,7 +9,7 @@ import { Select } from './Select/index.js'
import { Tabs } from './Tabs/index.js'
import { Text } from './Text/index.js'
export const diffComponents = {
export const diffComponents: Record<FieldTypes, React.ComponentType<FieldDiffClientProps>> = {
array: Iterable,
blocks: Iterable,
checkbox: Text,
@@ -16,6 +18,7 @@ export const diffComponents = {
date: Text,
email: Text,
group: Group,
join: null,
json: Text,
number: Text,
point: Text,
@@ -27,5 +30,6 @@ export const diffComponents = {
tabs: Tabs,
text: Text,
textarea: Text,
ui: null,
upload: Relationship,
}

View File

@@ -1,4 +1,7 @@
export const diffStyles = {
diffContainer: {
minWidth: 'unset',
},
variables: {
dark: {
addedBackground: 'var(--theme-success-900)',

View File

@@ -1,24 +0,0 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientField, SanitizedFieldPermissions } from 'payload'
import type React from 'react'
import type { DiffMethod } from 'react-diff-viewer-continued'
export type DiffComponents = Record<string, React.FC<DiffComponentProps>>
export type DiffComponentProps<TField extends ClientField = ClientField> = {
readonly comparison: any
readonly diffComponents: DiffComponents
readonly diffMethod?: DiffMethod
readonly field: TField
readonly fieldPermissions?:
| {
[key: string]: SanitizedFieldPermissions
}
| true
readonly fields: ClientField[]
readonly i18n: I18nClient
readonly isRichText?: boolean
readonly locale?: string
readonly locales?: string[]
readonly version: any
}

View File

@@ -6,6 +6,10 @@
flex-direction: column;
gap: var(--base);
[role='banner'] {
display: none !important;
}
&__field {
overflow-wrap: anywhere;
display: flex;

View File

@@ -1,153 +1,8 @@
'use client'
import type { DiffMethod } from 'react-diff-viewer-continued'
import { buildVersionFields, type BuildVersionFieldsArgs } from './buildVersionFields.js'
import { RenderVersionFieldsToDiff } from './RenderVersionFieldsToDiff.js'
import { fieldAffectsData, fieldIsID } from 'payload/shared'
import React from 'react'
export const RenderDiff = (args: BuildVersionFieldsArgs): React.ReactNode => {
const { versionFields } = buildVersionFields(args)
import type { diffComponents as _diffComponents } from './fields/index.js'
import type { FieldDiffProps, Props } from './types.js'
import { diffMethods } from './fields/diffMethods.js'
import './index.scss'
const baseClass = 'render-field-diffs'
export const RenderFieldsToDiff: React.FC<Props> = ({
comparison,
diffComponents: __diffComponents,
fieldPermissions,
fields,
i18n,
locales,
version,
}) => {
// typing it as `as typeof _diffComponents` here ensures the TField generics of DiffComponentProps are respected.
// Without it, you could pass a UI field to the Tabs component, without it erroring
const diffComponents: typeof _diffComponents = __diffComponents as typeof _diffComponents
return (
<div className={baseClass}>
{fields?.map((field, i) => {
if (fieldIsID(field)) {
return null
}
const Component = diffComponents[field.type]
const isRichText = field.type === 'richText'
const diffMethod: DiffMethod = diffMethods[field.type] || 'CHARS'
if (Component) {
if (fieldAffectsData(field)) {
const fieldName = field.name
const valueIsObject = field.type === 'code' || field.type === 'json'
const versionValue = valueIsObject
? JSON.stringify(version?.[fieldName])
: version?.[fieldName]
const comparisonValue = valueIsObject
? JSON.stringify(comparison?.[fieldName])
: comparison?.[fieldName]
const hasPermission =
fieldPermissions === true ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.read
const subFieldPermissions =
fieldPermissions === true ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.fields
if (!hasPermission) {
return null
}
const baseCellProps: FieldDiffProps = {
comparison: comparisonValue,
diffComponents,
diffMethod,
field,
fieldPermissions: subFieldPermissions,
fields: 'fields' in field ? field?.fields : fields,
i18n,
isRichText,
locales,
version: versionValue,
}
if (field.localized) {
return (
<div className={`${baseClass}__field`} key={i}>
{locales.map((locale, index) => {
const versionLocaleValue = versionValue?.[locale]
const comparisonLocaleValue = comparisonValue?.[locale]
const cellProps = {
...baseCellProps,
comparison: comparisonLocaleValue,
version: versionLocaleValue,
}
return (
<div className={`${baseClass}__locale`} key={[locale, index].join('-')}>
<div className={`${baseClass}__locale-value`}>
<Component {...cellProps} locale={locale} />
</div>
</div>
)
})}
</div>
)
}
return (
<div className={`${baseClass}__field`} key={i}>
<Component {...baseCellProps} />
</div>
)
}
if (field.type === 'tabs' && 'tabs' in field) {
const Tabs = diffComponents.tabs
return (
<Tabs
comparison={comparison}
diffComponents={diffComponents}
field={field}
fieldPermissions={fieldPermissions}
fields={[]}
i18n={i18n}
key={i}
locales={locales}
version={version}
/>
)
}
// At this point, we are dealing with a field with subfields but no
// nested data, eg. row, collapsible, etc.
if ('fields' in field) {
return (
<Component
comparison={comparison}
diffComponents={diffComponents}
field={field}
fieldPermissions={fieldPermissions}
fields={field.fields}
i18n={i18n}
key={i}
locales={locales}
version={version}
/>
)
}
}
return null
})}
</div>
)
return <RenderVersionFieldsToDiff versionFields={versionFields} />
}

View File

@@ -1,25 +0,0 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientField, SanitizedFieldPermissions } from 'payload'
import type { DiffMethod } from 'react-diff-viewer-continued'
import type { DiffComponents } from './fields/types.js'
export type Props = {
readonly comparison: Record<string, any>
readonly diffComponents: DiffComponents
readonly fieldPermissions:
| {
[key: string]: SanitizedFieldPermissions
}
| true
readonly fields: ClientField[]
readonly i18n: I18nClient
readonly locales: string[]
readonly version: Record<string, any>
}
export type FieldDiffProps = {
diffMethod: DiffMethod
field: ClientField
isRichText: boolean
} & Props

View File

@@ -178,9 +178,11 @@ export function countChangedFieldsInRows({
const comparisonRow = comparisonRows?.[i] || {}
const versionRow = versionRows?.[i] || {}
const rowFields = getFieldsForRowComparison({
const { fields: rowFields } = getFieldsForRowComparison({
baseVersionField: { type: 'text', fields: [], path: '', schemaPath: '' }, // Doesn't matter, as we don't need the versionFields output here
comparisonRow,
field,
row: i,
versionRow,
})

View File

@@ -0,0 +1,68 @@
import type { ClientField, Field, Tab, TabAsFieldClient } from 'payload'
type Args = {
field: ClientField | Field | Tab | TabAsFieldClient
index: number
parentIndexPath: string
parentPath: string
parentSchemaPath: string
}
type FieldPaths = {
/**
* A string of '-' separated indexes representing where
* to find this field in a given field schema array.
* It will always be complete and accurate.
*/
indexPath: string
/**
* Path for this field relative to its position in the data.
*/
path: string
/**
* Path for this field relative to its position in the schema.
*/
schemaPath: string
}
export function getFieldPathsModified({
field,
index,
parentIndexPath,
parentPath,
parentSchemaPath,
}: Args): FieldPaths {
const parentPathSegments = parentPath.split('.')
const parentIsUnnamed = parentPathSegments[parentPathSegments.length - 1].startsWith('_index-')
const parentWithoutIndex = parentIsUnnamed
? parentPathSegments.slice(0, -1).join('.')
: parentPath
const parentPathToUse = parentIsUnnamed ? parentWithoutIndex : parentPath
const parentSchemaPathSegments = parentSchemaPath.split('.')
const parentSchemaIsUnnamed =
parentSchemaPathSegments[parentSchemaPathSegments.length - 1].startsWith('_index-')
const parentSchemaWithoutIndex = parentSchemaIsUnnamed
? parentSchemaPathSegments.slice(0, -1).join('.')
: parentSchemaPath
const parentSchemaPathToUse = parentSchemaIsUnnamed ? parentSchemaWithoutIndex : parentSchemaPath
if ('name' in field) {
return {
indexPath: '',
path: `${parentPathToUse ? parentPathToUse + '.' : ''}${field.name}`,
schemaPath: `${parentSchemaPathToUse ? parentSchemaPathToUse + '.' : ''}${field.name}`,
}
}
const indexSuffix = `_index-${`${parentIndexPath ? parentIndexPath + '-' : ''}${index}`}`
return {
indexPath: `${parentIndexPath ? parentIndexPath + '-' : ''}${index}`,
path: `${parentPathToUse ? parentPathToUse + '.' : ''}${indexSuffix}`,
schemaPath: `${!parentIsUnnamed && parentSchemaPathToUse ? parentSchemaPathToUse + '.' : ''}${indexSuffix}`,
}
}

View File

@@ -15,13 +15,15 @@ describe('getFieldsForRowComparison', () => {
fields: arrayFields,
}
const result = getFieldsForRowComparison({
const { fields } = getFieldsForRowComparison({
field,
versionRow: {},
comparisonRow: {},
row: 0,
baseVersionField: { fields: [] },
})
expect(result).toEqual(arrayFields)
expect(fields).toEqual(arrayFields)
})
})
@@ -46,13 +48,15 @@ describe('getFieldsForRowComparison', () => {
const versionRow = { blockType: 'blockA' }
const comparisonRow = { blockType: 'blockA' }
const result = getFieldsForRowComparison({
const { fields } = getFieldsForRowComparison({
field,
versionRow,
comparisonRow,
row: 0,
baseVersionField: { fields: [] },
})
expect(result).toEqual(blockAFields)
expect(fields).toEqual(blockAFields)
})
it('should return unique combined fields when block types differ', () => {
@@ -80,14 +84,16 @@ describe('getFieldsForRowComparison', () => {
const versionRow = { blockType: 'blockA' }
const comparisonRow = { blockType: 'blockB' }
const result = getFieldsForRowComparison({
const { fields } = getFieldsForRowComparison({
field,
versionRow,
comparisonRow,
row: 0,
baseVersionField: { fields: [] },
})
// Should contain all unique fields from both blocks
expect(result).toEqual([
expect(fields).toEqual([
{ name: 'a', type: 'text' },
{ name: 'b', type: 'text' },
{ name: 'c', type: 'text' },

View File

@@ -1,4 +1,10 @@
import type { ArrayFieldClient, BlocksFieldClient, ClientField } from 'payload'
import type {
ArrayFieldClient,
BaseVersionField,
BlocksFieldClient,
ClientField,
VersionField,
} from 'payload'
import { getUniqueListBy } from 'payload/shared'
@@ -9,21 +15,27 @@ import { getUniqueListBy } from 'payload/shared'
* because the fields from the version and comparison rows may differ.
*/
export function getFieldsForRowComparison({
baseVersionField,
comparisonRow,
field,
row,
versionRow,
}: {
baseVersionField: BaseVersionField
comparisonRow: any
field: ArrayFieldClient | BlocksFieldClient
row: number
versionRow: any
}) {
}): { fields: ClientField[]; versionFields: VersionField[] } {
let fields: ClientField[] = []
let versionFields: VersionField[] = []
if (field.type === 'array' && 'fields' in field) {
fields = field.fields
}
if (field.type === 'blocks') {
versionFields = baseVersionField.rows?.length
? baseVersionField.rows[row]
: baseVersionField.fields
} else if (field.type === 'blocks') {
if (versionRow?.blockType === comparisonRow?.blockType) {
const matchedBlock = ('blocks' in field &&
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
@@ -31,6 +43,9 @@ export function getFieldsForRowComparison({
}
fields = matchedBlock.fields
versionFields = baseVersionField.rows?.length
? baseVersionField.rows[row]
: baseVersionField.fields
} else {
const matchedVersionBlock = ('blocks' in field &&
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
@@ -45,8 +60,13 @@ export function getFieldsForRowComparison({
[...matchedVersionBlock.fields, ...matchedComparisonBlock.fields],
'name',
)
// buildVersionFields already merged the fields of the version and comparison rows together
versionFields = baseVersionField.rows?.length
? baseVersionField.rows[row]
: baseVersionField.fields
}
}
return fields
return { fields, versionFields }
}

View File

@@ -4,7 +4,7 @@ import type { PaginatedDocs, Where } from 'payload'
import { fieldBaseClass, Pill, ReactSelect, useConfig, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import * as qs from 'qs-esm'
import { stringify } from 'qs-esm'
import React, { useCallback, useEffect, useState } from 'react'
import type { Props } from './types.js'
@@ -87,7 +87,7 @@ export const SelectComparison: React.FC<Props> = (props) => {
})
}
const search = qs.stringify(query)
const search = stringify(query)
const response = await fetch(`${baseURL}?${search}`, {
credentials: 'include',
@@ -163,8 +163,12 @@ export const SelectComparison: React.FC<Props> = (props) => {
)
useEffect(() => {
if (!i18n.dateFNS) {
// If dateFNS is not loaded, we can't format the date in getResults
return
}
void getResults({ lastLoadedPage: 1 })
}, [getResults])
}, [getResults, i18n.dateFNS])
const filteredOptions = options.filter(
(option, index, self) => self.findIndex((t) => t.value === option.value) === index,

View File

@@ -1,4 +1,4 @@
import type { PaginatedDocs, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
import type { PaginatedDocs, SanitizedCollectionConfig } from 'payload'
import type { CompareOption } from '../Default/types.js'

View File

@@ -7,14 +7,18 @@ import type {
SanitizedGlobalPermission,
} from 'payload'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { getClientSchemaMap } from '@payloadcms/ui/utilities/getClientSchemaMap'
import { getSchemaMap } from '@payloadcms/ui/utilities/getSchemaMap'
import { notFound } from 'next/navigation.js'
import React from 'react'
import { getLatestVersion } from '../Versions/getLatestVersion.js'
import { DefaultVersionView } from './Default/index.js'
import { RenderDiff } from './RenderFieldsToDiff/index.js'
export const VersionView: PayloadServerReactComponent<EditViewComponent> = async (props) => {
const { initPageResult, routeSegments } = props
const { i18n, initPageResult, routeSegments, searchParams } = props
const {
collectionConfig,
@@ -30,6 +34,13 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
const collectionSlug = collectionConfig?.slug
const globalSlug = globalConfig?.slug
const localeCodesFromParams = searchParams.localeCodes
? JSON.parse(searchParams.localeCodes as string)
: null
const comparisonVersionIDFromParams: string = searchParams.compareValue as string
const modifiedOnly: boolean = searchParams.modifiedOnly === 'true'
const { localization } = config
let docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
@@ -48,8 +59,8 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
doc = await payload.findVersionByID({
id: versionID,
collection: slug,
depth: 1,
locale: '*',
depth: 0,
locale: 'all',
overrideAccess: false,
req,
user,
@@ -59,15 +70,21 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
latestDraftVersion = await getLatestVersion({
slug,
type: 'collection',
locale: 'all',
overrideAccess: false,
parentID: id,
payload,
req,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'collection',
locale: 'all',
overrideAccess: false,
parentID: id,
payload,
req,
status: 'published',
})
}
@@ -85,8 +102,8 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
doc = await payload.findGlobalVersionByID({
id: versionID,
slug,
depth: 1,
locale: '*',
depth: 0,
locale: 'all',
overrideAccess: false,
req,
user,
@@ -96,13 +113,19 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
latestDraftVersion = await getLatestVersion({
slug,
type: 'global',
locale: 'all',
overrideAccess: false,
payload,
req,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'global',
locale: 'all',
overrideAccess: false,
payload,
req,
status: 'published',
})
}
@@ -120,12 +143,27 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
}
}
const localeOptions: OptionObject[] =
localization &&
localization.locales.map(({ code, label }) => ({
label,
value: code,
}))
const selectedLocales: OptionObject[] = []
if (localization) {
if (localeCodesFromParams) {
for (const code of localeCodesFromParams) {
const locale = localization.locales.find((locale) => locale.code === code)
if (locale) {
selectedLocales.push({
label: locale.label,
value: locale.code,
})
}
}
} else {
for (const { code, label } of localization.locales) {
selectedLocales.push({
label,
value: code,
})
}
}
}
const latestVersion =
latestPublishedVersion?.updatedAt > latestDraftVersion?.updatedAt
@@ -136,14 +174,83 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
return notFound()
}
/**
* The doc to compare this version to is either the latest version, or a specific version if specified in the URL.
* This specific version is added to the URL when a user selects a version to compare to.
*/
let comparisonDoc = null
if (comparisonVersionIDFromParams) {
if (collectionSlug) {
comparisonDoc = await payload.findVersionByID({
id: comparisonVersionIDFromParams,
collection: collectionSlug,
depth: 0,
locale: 'all',
overrideAccess: false,
req,
})
} else {
comparisonDoc = await payload.findGlobalVersionByID({
id: comparisonVersionIDFromParams,
slug: globalSlug,
depth: 0,
locale: 'all',
overrideAccess: false,
req,
})
}
} else {
comparisonDoc = latestVersion
}
const schemaMap = getSchemaMap({
collectionSlug,
config,
globalSlug,
i18n,
})
const clientSchemaMap = getClientSchemaMap({
collectionSlug,
config: getClientConfig({ config: payload.config, i18n, importMap: payload.importMap }),
globalSlug,
i18n,
payload,
schemaMap,
})
const RenderedDiff = RenderDiff({
clientSchemaMap,
comparisonSiblingData: comparisonDoc?.version,
customDiffComponents: {},
entitySlug: collectionSlug || globalSlug,
fieldPermissions: docPermissions?.fields,
fields: (collectionConfig || globalConfig)?.fields,
i18n,
modifiedOnly,
parentIndexPath: '',
parentPath: '',
parentSchemaPath: '',
req,
selectedLocales: selectedLocales && selectedLocales.map((locale) => locale.value),
versionSiblingData: globalConfig
? {
...doc?.version,
createdAt: doc?.version?.createdAt || doc.createdAt,
updatedAt: doc?.version?.updatedAt || doc.updatedAt,
}
: doc?.version,
})
return (
<DefaultVersionView
canUpdate={docPermissions?.update}
doc={doc}
docPermissions={docPermissions}
initialComparisonDoc={latestVersion}
latestDraftVersion={latestDraftVersion?.id}
latestPublishedVersion={latestPublishedVersion?.id}
localeOptions={localeOptions}
modifiedOnly={modifiedOnly}
RenderedDiff={RenderedDiff}
selectedLocales={selectedLocales}
versionID={versionID}
/>
)

View File

@@ -1,4 +1,4 @@
import type { Payload, Where } from 'payload'
import type { Payload, PayloadRequest, Where } from 'payload'
import { logError } from 'payload'
@@ -8,14 +8,17 @@ type ReturnType = {
} | null
type Args = {
locale?: string
overrideAccess?: boolean
parentID?: number | string
payload: Payload
req?: PayloadRequest
slug: string
status: 'draft' | 'published'
type: 'collection' | 'global'
}
export async function getLatestVersion(args: Args): Promise<ReturnType> {
const { slug, type = 'collection', parentID, payload, status } = args
const { slug, type = 'collection', locale, overrideAccess, parentID, payload, req, status } = args
const and: Where[] = [
{
@@ -37,6 +40,9 @@ export async function getLatestVersion(args: Args): Promise<ReturnType> {
const sharedOptions = {
depth: 0,
limit: 1,
locale,
overrideAccess,
req,
sort: '-updatedAt',
where: {
and,

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.19.0",
"version": "3.20.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -126,7 +126,7 @@
"@types/ws": "^8.5.10",
"copyfiles": "2.4.1",
"cross-env": "7.0.3",
"esbuild": "0.24.0",
"esbuild": "0.24.2",
"graphql-http": "^1.22.0",
"react-datepicker": "7.6.0",
"rimraf": "6.0.1",

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -53,7 +55,6 @@ export type ArrayFieldDescriptionServerComponent = FieldDescriptionServerCompone
ArrayField,
ArrayFieldClientWithoutType
>
export type ArrayFieldDescriptionClientComponent =
FieldDescriptionClientComponent<ArrayFieldClientWithoutType>
@@ -61,5 +62,7 @@ export type ArrayFieldErrorServerComponent = FieldErrorServerComponent<
ArrayField,
ArrayFieldClientWithoutType
>
export type ArrayFieldErrorClientComponent = FieldErrorClientComponent<ArrayFieldClientWithoutType>
export type ArrayFieldDiffServerComponent = FieldDiffServerComponent<ArrayField, ArrayFieldClient>
export type ArrayFieldDiffClientComponent = FieldDiffClientComponent<ArrayFieldClient>

View File

@@ -14,6 +14,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -80,3 +82,10 @@ export type BlocksFieldErrorServerComponent = FieldErrorServerComponent<
export type BlocksFieldErrorClientComponent =
FieldErrorClientComponent<BlocksFieldClientWithoutType>
export type BlocksFieldDiffServerComponent = FieldDiffServerComponent<
BlocksField,
BlocksFieldClient
>
export type BlocksFieldDiffClientComponent = FieldDiffClientComponent<BlocksFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -71,3 +73,10 @@ export type CheckboxFieldErrorServerComponent = FieldErrorServerComponent<
export type CheckboxFieldErrorClientComponent =
FieldErrorClientComponent<CheckboxFieldClientWithoutType>
export type CheckboxFieldDiffServerComponent = FieldDiffServerComponent<
CheckboxField,
CheckboxFieldClient
>
export type CheckboxFieldDiffClientComponent = FieldDiffClientComponent<CheckboxFieldClient>

View File

@@ -14,6 +14,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -67,3 +69,7 @@ export type CodeFieldErrorServerComponent = FieldErrorServerComponent<
>
export type CodeFieldErrorClientComponent = FieldErrorClientComponent<CodeFieldClientWithoutType>
export type CodeFieldDiffServerComponent = FieldDiffServerComponent<CodeField, CodeFieldClient>
export type CodeFieldDiffClientComponent = FieldDiffClientComponent<CodeFieldClient>

View File

@@ -12,6 +12,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -61,3 +63,10 @@ export type CollapsibleFieldErrorServerComponent = FieldErrorServerComponent<
export type CollapsibleFieldErrorClientComponent =
FieldErrorClientComponent<CollapsibleFieldClientWithoutType>
export type CollapsibleFieldDiffServerComponent = FieldDiffServerComponent<
CollapsibleField,
CollapsibleFieldClient
>
export type CollapsibleFieldDiffClientComponent = FieldDiffClientComponent<CollapsibleFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -64,3 +66,7 @@ export type DateFieldErrorServerComponent = FieldErrorServerComponent<
>
export type DateFieldErrorClientComponent = FieldErrorClientComponent<DateFieldClientWithoutType>
export type DateFieldDiffServerComponent = FieldDiffServerComponent<DateField, DateFieldClient>
export type DateFieldDiffClientComponent = FieldDiffClientComponent<DateFieldClient>

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