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
This commit is contained in:
Alessio Gravili
2025-01-28 15:17:24 -07:00
committed by GitHub
parent 33ac13df28
commit c562fbfa94
70 changed files with 11006 additions and 3702 deletions

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:

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

@@ -1,36 +1,45 @@
'use client'
import type { OptionObject } from 'payload'
import {
CheckboxInput,
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()
@@ -38,13 +47,44 @@ 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 [modifiedOnly, setModifiedOnly] = useState(false)
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 },
localization,
@@ -60,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 (
@@ -129,29 +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}
modifiedOnly={modifiedOnly}
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,62 @@
'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`} 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,417 @@
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') {
if (!Array.isArray(versionValue)) {
throw new Error('Expected versionValue to be an array')
}
baseVersionField.rows = []
for (let i = 0; i < versionValue.length; i++) {
const comparisonRow = comparisonValue?.[i] || {}
const versionRow = versionValue?.[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 = []
if (!Array.isArray(versionValue)) {
throw new Error('Expected versionValue to be an array')
}
for (let i = 0; i < versionValue.length; i++) {
const comparisonRow = comparisonValue?.[i] || {}
const versionRow = versionValue[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,32 +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,
modifiedOnly,
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)) {
@@ -36,7 +38,7 @@ export const Iterable: React.FC<DiffComponentProps> = ({
return (
<div className={baseClass}>
<DiffCollapser
comparison={comparison}
comparison={comparisonValue}
field={field}
isIterable
label={
@@ -49,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,
})
@@ -73,19 +77,11 @@ 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}
modifiedOnly
version={versionRow}
/></DiffCollapser>
<RenderVersionFieldsToDiff versionFields={versionFields} />
</DiffCollapser>
</div>
)
})}

View File

@@ -1,17 +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 ReactDiffViewer from 'react-diff-viewer-continued'
import type { DiffComponentProps } from '../types.js'
import Label from '../../Label/index.js'
import { diffStyles } from '../styles.js'
import './index.scss'
import { diffStyles } from '../styles.js'
const baseClass = 'relationship-diff'
@@ -96,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 {
@@ -112,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,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,24 +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,
modifiedOnly,
tab,
version,
}) => {
return (
<DiffCollapser
comparison={comparison}
fields={tab.fields}
comparison={comparisonValue}
fields={fieldTab.fields}
label={
'label' in tab &&
tab.label &&
@@ -85,19 +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}
modifiedOnly={modifiedOnly}
version={version}
/>
<RenderVersionFieldsToDiff versionFields={tab.fields} />
</DiffCollapser>
)
}

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,25 +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 modifiedOnly?: boolean
readonly version: any
}

View File

@@ -1,177 +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 { ShimmerEffect } from '@payloadcms/ui'
import { dequal } from 'dequal/lite'
import { fieldAffectsData, fieldIsID } from 'payload/shared'
import React, { Fragment, useEffect } 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,
modifiedOnly,
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
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>
) : (
<Fragment>
{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]
if (modifiedOnly && dequal(versionValue, comparisonValue)) {
return null
}
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,
modifiedOnly,
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
})}
</Fragment>
)}
</div>
)
return <RenderVersionFieldsToDiff versionFields={versionFields} />
}

View File

@@ -1,26 +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 modifiedOnly?: boolean
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

@@ -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>

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 EmailFieldErrorServerComponent = FieldErrorServerComponent<
>
export type EmailFieldErrorClientComponent = FieldErrorClientComponent<EmailFieldClientWithoutType>
export type EmailFieldDiffServerComponent = FieldDiffServerComponent<EmailField, EmailFieldClient>
export type EmailFieldDiffClientComponent = FieldDiffClientComponent<EmailFieldClient>

View File

@@ -12,6 +12,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -60,3 +62,7 @@ export type GroupFieldErrorServerComponent = FieldErrorServerComponent<
>
export type GroupFieldErrorClientComponent = FieldErrorClientComponent<GroupFieldClientWithoutType>
export type GroupFieldDiffServerComponent = FieldDiffServerComponent<GroupField, GroupFieldClient>
export type GroupFieldDiffClientComponent = FieldDiffClientComponent<GroupFieldClient>

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 JSONFieldErrorServerComponent = FieldErrorServerComponent<
>
export type JSONFieldErrorClientComponent = FieldErrorClientComponent<JSONFieldClientWithoutType>
export type JSONFieldDiffServerComponent = FieldDiffServerComponent<JSONField, JSONFieldClient>
export type JSONFieldDiffClientComponent = FieldDiffClientComponent<JSONFieldClient>

View File

@@ -12,6 +12,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -44,11 +46,21 @@ export type JoinFieldLabelServerComponent = FieldLabelServerComponent<JoinField>
export type JoinFieldLabelClientComponent = FieldLabelClientComponent<JoinFieldClientWithoutType>
export type JoinFieldDescriptionServerComponent = FieldDescriptionServerComponent<JoinField>
export type JoinFieldDescriptionServerComponent = FieldDescriptionServerComponent<
JoinField,
JoinFieldClientWithoutType
>
export type JoinFieldDescriptionClientComponent =
FieldDescriptionClientComponent<JoinFieldClientWithoutType>
export type JoinFieldErrorServerComponent = FieldErrorServerComponent<JoinField>
export type JoinFieldErrorServerComponent = FieldErrorServerComponent<
JoinField,
JoinFieldClientWithoutType
>
export type JoinFieldErrorClientComponent = FieldErrorClientComponent<JoinFieldClientWithoutType>
export type JoinFieldDiffServerComponent = FieldDiffServerComponent<JoinField, JoinFieldClient>
export type JoinFieldDiffClientComponent = FieldDiffClientComponent<JoinFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -67,3 +69,10 @@ export type NumberFieldErrorServerComponent = FieldErrorServerComponent<
export type NumberFieldErrorClientComponent =
FieldErrorClientComponent<NumberFieldClientWithoutType>
export type NumberFieldDiffServerComponent = FieldDiffServerComponent<
NumberField,
NumberFieldClient
>
export type NumberFieldDiffClientComponent = FieldDiffClientComponent<NumberFieldClient>

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 PointFieldErrorServerComponent = FieldErrorServerComponent<
>
export type PointFieldErrorClientComponent = FieldErrorClientComponent<PointFieldClientWithoutType>
export type PointFieldDiffServerComponent = FieldDiffServerComponent<PointField, PointFieldClient>
export type PointFieldDiffClientComponent = FieldDiffClientComponent<PointFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -49,7 +51,7 @@ export type RadioFieldClientComponent = FieldClientComponent<
RadioFieldBaseClientProps
>
export type OnChange<T = string> = (value: T) => void
type OnChange<T = string> = (value: T) => void
export type RadioFieldLabelServerComponent = FieldLabelServerComponent<
RadioField,
@@ -72,3 +74,7 @@ export type RadioFieldErrorServerComponent = FieldErrorServerComponent<
>
export type RadioFieldErrorClientComponent = FieldErrorClientComponent<RadioFieldClientWithoutType>
export type RadioFieldDiffServerComponent = FieldDiffServerComponent<RadioField, RadioFieldClient>
export type RadioFieldDiffClientComponent = FieldDiffClientComponent<RadioFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -66,3 +68,10 @@ export type RelationshipFieldErrorServerComponent = FieldErrorServerComponent<
export type RelationshipFieldErrorClientComponent =
FieldErrorClientComponent<RelationshipFieldClientWithoutType>
export type RelationshipFieldDiffServerComponent = FieldDiffServerComponent<
RelationshipField,
RelationshipFieldClient
>
export type RelationshipFieldDiffClientComponent = FieldDiffClientComponent<RelationshipFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -78,3 +80,10 @@ export type RichTextFieldErrorServerComponent = FieldErrorServerComponent<
export type RichTextFieldErrorClientComponent =
FieldErrorClientComponent<RichTextFieldClientWithoutType>
export type RichTextFieldDiffServerComponent = FieldDiffServerComponent<
RichTextField,
RichTextFieldClient
>
export type RichTextFieldDiffClientComponent = FieldDiffClientComponent<RichTextFieldClient>

View File

@@ -11,6 +11,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldErrorClientComponent,
FieldErrorServerComponent,
FieldLabelClientComponent,
@@ -56,3 +58,7 @@ export type RowFieldErrorServerComponent = FieldErrorServerComponent<
>
export type RowFieldErrorClientComponent = FieldErrorClientComponent<RowFieldClientWithoutType>
export type RowFieldDiffServerComponent = FieldDiffServerComponent<RowField, RowFieldClient>
export type RowFieldDiffClientComponent = FieldDiffClientComponent<RowFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -68,3 +70,10 @@ export type SelectFieldErrorServerComponent = FieldErrorServerComponent<
export type SelectFieldErrorClientComponent =
FieldErrorClientComponent<SelectFieldClientWithoutType>
export type SelectFieldDiffServerComponent = FieldDiffServerComponent<
SelectField,
SelectFieldClient
>
export type SelectFieldDiffClientComponent = FieldDiffClientComponent<SelectFieldClient>

View File

@@ -18,6 +18,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -63,3 +65,7 @@ export type TabsFieldErrorServerComponent = FieldErrorServerComponent<
>
export type TabsFieldErrorClientComponent = FieldErrorClientComponent<TabsFieldClientWithoutType>
export type TabsFieldDiffServerComponent = FieldDiffServerComponent<TabsField, TabsFieldClient>
export type TabsFieldDiffClientComponent = FieldDiffClientComponent<TabsFieldClient>

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 TextFieldErrorServerComponent = FieldErrorServerComponent<
>
export type TextFieldErrorClientComponent = FieldErrorClientComponent<TextFieldClientWithoutType>
export type TextFieldDiffServerComponent = FieldDiffServerComponent<TextField, TextFieldClient>
export type TextFieldDiffClientComponent = FieldDiffClientComponent<TextFieldClient>

View File

@@ -14,6 +14,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -72,3 +74,10 @@ export type TextareaFieldErrorServerComponent = FieldErrorServerComponent<
export type TextareaFieldErrorClientComponent =
FieldErrorClientComponent<TextareaFieldClientWithoutType>
export type TextareaFieldDiffServerComponent = FieldDiffServerComponent<
TextareaField,
TextareaFieldClient
>
export type TextareaFieldDiffClientComponent = FieldDiffClientComponent<TextareaFieldClient>

View File

@@ -4,6 +4,8 @@ import type { UIField, UIFieldClient } from '../../fields/config/types.js'
import type {
ClientFieldBase,
FieldClientComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldPaths,
FieldServerComponent,
ServerFieldBase,
@@ -32,3 +34,7 @@ export type UIFieldServerComponent = FieldServerComponent<
UIFieldClientWithoutType,
UIFieldBaseServerProps
>
export type UIFieldDiffServerComponent = FieldDiffServerComponent<UIField, UIFieldClient>
export type UIFieldDiffClientComponent = FieldDiffClientComponent<UIFieldClient>

View File

@@ -13,6 +13,8 @@ import type {
import type {
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDiffClientComponent,
FieldDiffServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
} from '../types.js'
@@ -66,3 +68,10 @@ export type UploadFieldErrorServerComponent = FieldErrorServerComponent<
export type UploadFieldErrorClientComponent =
FieldErrorClientComponent<UploadFieldClientWithoutType>
export type UploadFieldDiffServerComponent = FieldDiffServerComponent<
UploadField,
UploadFieldClient
>
export type UploadFieldDiffClientComponent = FieldDiffClientComponent<UploadFieldClient>

View File

@@ -0,0 +1,86 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientField, Field, FieldTypes, Tab } from '../../fields/config/types.js'
import type {
ClientFieldWithOptionalType,
PayloadRequest,
SanitizedFieldPermissions,
TypedLocale,
} from '../../index.js'
export type VersionTab = {
fields: VersionField[]
name?: string
} & Pick<Tab, 'label'>
export type BaseVersionField = {
CustomComponent?: React.ReactNode
fields: VersionField[]
path: string
rows?: VersionField[][]
schemaPath: string
tabs?: VersionTab[]
type: FieldTypes
}
export type VersionField = {
field?: BaseVersionField
fieldByLocale?: Record<TypedLocale, BaseVersionField>
}
/**
* Taken from react-diff-viewer-continued
*/
export declare enum DiffMethod {
CHARS = 'diffChars',
CSS = 'diffCss',
JSON = 'diffJson',
LINES = 'diffLines',
SENTENCES = 'diffSentences',
TRIMMED_LINES = 'diffTrimmedLines',
WORDS = 'diffWords',
WORDS_WITH_SPACE = 'diffWordsWithSpace',
}
export type FieldDiffClientProps<TClientField extends ClientFieldWithOptionalType = ClientField> = {
baseVersionField: BaseVersionField
/**
* Field value from the version being compared
*/
comparisonValue: unknown
diffMethod: DiffMethod
field: TClientField
fieldPermissions:
| {
[key: string]: SanitizedFieldPermissions
}
| true
/**
* If this field is localized, this will be the locale of the field
*/
locale?: string
/**
* Field value from the current version
*/
versionValue: unknown
}
export type FieldDiffServerProps<
TField extends Field = Field,
TClientField extends ClientFieldWithOptionalType = ClientField,
> = {
clientField: TClientField
field: TField
i18n: I18nClient
req: PayloadRequest
selectedLocales: string[]
} & Omit<FieldDiffClientProps, 'field'>
export type FieldDiffClientComponent<
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
> = React.ComponentType<FieldDiffClientProps<TFieldClient>>
export type FieldDiffServerComponent<
TFieldServer extends Field = Field,
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
> = React.ComponentType<FieldDiffServerProps<TFieldServer, TFieldClient>>

View File

@@ -51,6 +51,8 @@ export type {
ArrayFieldClientProps,
ArrayFieldDescriptionClientComponent,
ArrayFieldDescriptionServerComponent,
ArrayFieldDiffClientComponent,
ArrayFieldDiffServerComponent,
ArrayFieldErrorClientComponent,
ArrayFieldErrorServerComponent,
ArrayFieldLabelClientComponent,
@@ -66,6 +68,8 @@ export type {
BlocksFieldClientProps,
BlocksFieldDescriptionClientComponent,
BlocksFieldDescriptionServerComponent,
BlocksFieldDiffClientComponent,
BlocksFieldDiffServerComponent,
BlocksFieldErrorClientComponent,
BlocksFieldErrorServerComponent,
BlocksFieldLabelClientComponent,
@@ -79,6 +83,8 @@ export type {
CheckboxFieldClientProps,
CheckboxFieldDescriptionClientComponent,
CheckboxFieldDescriptionServerComponent,
CheckboxFieldDiffClientComponent,
CheckboxFieldDiffServerComponent,
CheckboxFieldErrorClientComponent,
CheckboxFieldErrorServerComponent,
CheckboxFieldLabelClientComponent,
@@ -92,6 +98,8 @@ export type {
CodeFieldClientProps,
CodeFieldDescriptionClientComponent,
CodeFieldDescriptionServerComponent,
CodeFieldDiffClientComponent,
CodeFieldDiffServerComponent,
CodeFieldErrorClientComponent,
CodeFieldErrorServerComponent,
CodeFieldLabelClientComponent,
@@ -105,6 +113,8 @@ export type {
CollapsibleFieldClientProps,
CollapsibleFieldDescriptionClientComponent,
CollapsibleFieldDescriptionServerComponent,
CollapsibleFieldDiffClientComponent,
CollapsibleFieldDiffServerComponent,
CollapsibleFieldErrorClientComponent,
CollapsibleFieldErrorServerComponent,
CollapsibleFieldLabelClientComponent,
@@ -118,6 +128,8 @@ export type {
DateFieldClientProps,
DateFieldDescriptionClientComponent,
DateFieldDescriptionServerComponent,
DateFieldDiffClientComponent,
DateFieldDiffServerComponent,
DateFieldErrorClientComponent,
DateFieldErrorServerComponent,
DateFieldLabelClientComponent,
@@ -131,6 +143,8 @@ export type {
EmailFieldClientProps,
EmailFieldDescriptionClientComponent,
EmailFieldDescriptionServerComponent,
EmailFieldDiffClientComponent,
EmailFieldDiffServerComponent,
EmailFieldErrorClientComponent,
EmailFieldErrorServerComponent,
EmailFieldLabelClientComponent,
@@ -144,6 +158,8 @@ export type {
GroupFieldClientProps,
GroupFieldDescriptionClientComponent,
GroupFieldDescriptionServerComponent,
GroupFieldDiffClientComponent,
GroupFieldDiffServerComponent,
GroupFieldErrorClientComponent,
GroupFieldErrorServerComponent,
GroupFieldLabelClientComponent,
@@ -159,6 +175,8 @@ export type {
JoinFieldClientProps,
JoinFieldDescriptionClientComponent,
JoinFieldDescriptionServerComponent,
JoinFieldDiffClientComponent,
JoinFieldDiffServerComponent,
JoinFieldErrorClientComponent,
JoinFieldErrorServerComponent,
JoinFieldLabelClientComponent,
@@ -172,6 +190,8 @@ export type {
JSONFieldClientProps,
JSONFieldDescriptionClientComponent,
JSONFieldDescriptionServerComponent,
JSONFieldDiffClientComponent,
JSONFieldDiffServerComponent,
JSONFieldErrorClientComponent,
JSONFieldErrorServerComponent,
JSONFieldLabelClientComponent,
@@ -185,6 +205,8 @@ export type {
NumberFieldClientProps,
NumberFieldDescriptionClientComponent,
NumberFieldDescriptionServerComponent,
NumberFieldDiffClientComponent,
NumberFieldDiffServerComponent,
NumberFieldErrorClientComponent,
NumberFieldErrorServerComponent,
NumberFieldLabelClientComponent,
@@ -198,6 +220,8 @@ export type {
PointFieldClientProps,
PointFieldDescriptionClientComponent,
PointFieldDescriptionServerComponent,
PointFieldDiffClientComponent,
PointFieldDiffServerComponent,
PointFieldErrorClientComponent,
PointFieldErrorServerComponent,
PointFieldLabelClientComponent,
@@ -211,6 +235,8 @@ export type {
RadioFieldClientProps,
RadioFieldDescriptionClientComponent,
RadioFieldDescriptionServerComponent,
RadioFieldDiffClientComponent,
RadioFieldDiffServerComponent,
RadioFieldErrorClientComponent,
RadioFieldErrorServerComponent,
RadioFieldLabelClientComponent,
@@ -224,6 +250,8 @@ export type {
RelationshipFieldClientProps,
RelationshipFieldDescriptionClientComponent,
RelationshipFieldDescriptionServerComponent,
RelationshipFieldDiffClientComponent,
RelationshipFieldDiffServerComponent,
RelationshipFieldErrorClientComponent,
RelationshipFieldErrorServerComponent,
RelationshipFieldLabelClientComponent,
@@ -237,6 +265,8 @@ export type {
RichTextFieldClientProps,
RichTextFieldDescriptionClientComponent,
RichTextFieldDescriptionServerComponent,
RichTextFieldDiffClientComponent,
RichTextFieldDiffServerComponent,
RichTextFieldErrorClientComponent,
RichTextFieldErrorServerComponent,
RichTextFieldLabelClientComponent,
@@ -250,6 +280,8 @@ export type {
RowFieldClientProps,
RowFieldDescriptionClientComponent,
RowFieldDescriptionServerComponent,
RowFieldDiffClientComponent,
RowFieldDiffServerComponent,
RowFieldErrorClientComponent,
RowFieldErrorServerComponent,
RowFieldLabelClientComponent,
@@ -263,6 +295,8 @@ export type {
SelectFieldClientProps,
SelectFieldDescriptionClientComponent,
SelectFieldDescriptionServerComponent,
SelectFieldDiffClientComponent,
SelectFieldDiffServerComponent,
SelectFieldErrorClientComponent,
SelectFieldErrorServerComponent,
SelectFieldLabelClientComponent,
@@ -277,6 +311,8 @@ export type {
TabsFieldClientProps,
TabsFieldDescriptionClientComponent,
TabsFieldDescriptionServerComponent,
TabsFieldDiffClientComponent,
TabsFieldDiffServerComponent,
TabsFieldErrorClientComponent,
TabsFieldErrorServerComponent,
TabsFieldLabelClientComponent,
@@ -290,6 +326,8 @@ export type {
TextFieldClientProps,
TextFieldDescriptionClientComponent,
TextFieldDescriptionServerComponent,
TextFieldDiffClientComponent,
TextFieldDiffServerComponent,
TextFieldErrorClientComponent,
TextFieldErrorServerComponent,
TextFieldLabelClientComponent,
@@ -303,6 +341,8 @@ export type {
TextareaFieldClientProps,
TextareaFieldDescriptionClientComponent,
TextareaFieldDescriptionServerComponent,
TextareaFieldDiffClientComponent,
TextareaFieldDiffServerComponent,
TextareaFieldErrorClientComponent,
TextareaFieldErrorServerComponent,
TextareaFieldLabelClientComponent,
@@ -314,6 +354,8 @@ export type {
export type {
UIFieldClientComponent,
UIFieldClientProps,
UIFieldDiffClientComponent,
UIFieldDiffServerComponent,
UIFieldServerComponent,
UIFieldServerProps,
} from './fields/UI.js'
@@ -323,6 +365,8 @@ export type {
UploadFieldClientProps,
UploadFieldDescriptionClientComponent,
UploadFieldDescriptionServerComponent,
UploadFieldDiffClientComponent,
UploadFieldDiffServerComponent,
UploadFieldErrorClientComponent,
UploadFieldErrorServerComponent,
UploadFieldLabelClientComponent,
@@ -342,6 +386,17 @@ export type {
StaticDescription,
} from './forms/Description.js'
export type {
BaseVersionField,
DiffMethod,
FieldDiffClientComponent,
FieldDiffClientProps,
FieldDiffServerComponent,
FieldDiffServerProps,
VersionField,
VersionTab,
} from './forms/Diff.js'
export type {
FieldErrorClientComponent,
FieldErrorClientProps,

View File

@@ -110,5 +110,7 @@ export function genImportMapIterateFields({
hasKey(field?.admin?.components, 'RowLabel') &&
addToImportMap(field?.admin?.components?.RowLabel)
hasKey(field?.admin?.components, 'Diff') && addToImportMap(field?.admin?.components?.Diff)
}
}

View File

@@ -45,14 +45,18 @@ import type {
DateFieldErrorServerComponent,
DateFieldLabelClientComponent,
DateFieldLabelServerComponent,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Description,
EmailFieldClientProps,
EmailFieldErrorClientComponent,
EmailFieldErrorServerComponent,
EmailFieldLabelClientComponent,
EmailFieldLabelServerComponent,
FieldDescriptionClientComponent,
FieldDescriptionServerComponent,
FieldDescriptionClientProps,
FieldDescriptionServerProps,
FieldDiffClientComponent,
FieldDiffServerProps,
GroupFieldClientProps,
GroupFieldLabelClientComponent,
GroupFieldLabelServerComponent,
@@ -269,9 +273,10 @@ export type FilterOptions<TData = any> =
type Admin = {
className?: string
components?: {
Cell?: CustomComponent
Description?: CustomComponent<FieldDescriptionClientComponent | FieldDescriptionServerComponent>
Field?: CustomComponent<FieldClientComponent | FieldServerComponent>
Cell?: PayloadComponent<DefaultServerCellComponentProps, DefaultCellComponentProps>
Description?: PayloadComponent<FieldDescriptionServerProps, FieldDescriptionClientProps>
Diff?: PayloadComponent<FieldDiffServerProps, FieldDiffClientComponent>
Field?: PayloadComponent<FieldClientComponent | FieldServerComponent>
/**
* The Filter component has to be a client component
*/

View File

@@ -53,6 +53,16 @@
"types": "./src/utilities/buildTableState.ts",
"default": "./src/utilities/buildTableState.ts"
},
"./utilities/getClientSchemaMap": {
"import": "./src/utilities/getClientSchemaMap.ts",
"types": "./src/utilities/getClientSchemaMap.ts",
"default": "./src/utilities/getClientSchemaMap.ts"
},
"./utilities/getSchemaMap": {
"import": "./src/utilities/getSchemaMap.ts",
"types": "./src/utilities/getSchemaMap.ts",
"default": "./src/utilities/getSchemaMap.ts"
},
"./utilities/schedulePublishHandler": {
"import": "./src/utilities/schedulePublishHandler.ts",
"types": "./src/utilities/schedulePublishHandler.ts",

11807
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,20 @@
import path from 'path'
import { type Payload } from 'payload'
import { fileURLToPath } from 'url'
import { seedDB } from '../helpers/seed.js'
import { seed } from './seed.js'
import { collectionSlugs } from './slugs.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export async function clearAndSeedEverything(_payload: Payload, parallel: boolean = false) {
return await seedDB({
snapshotKey: 'versionsTest',
collectionSlugs,
_payload,
uploadsDir: path.resolve(dirname, './collections/uploads'),
seedFunction: async (_payload) => {
await seed(_payload, parallel)
},

View File

@@ -0,0 +1,172 @@
import type { CollectionConfig } from 'payload'
import { diffCollectionSlug, draftCollectionSlug } from '../slugs.js'
export const Diff: CollectionConfig = {
slug: diffCollectionSlug,
fields: [
{
name: 'array',
type: 'array',
fields: [
{
name: 'textInArray',
type: 'text',
},
],
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'TextBlock',
fields: [
{
name: 'textInBlock',
type: 'text',
},
],
},
],
},
{
type: 'checkbox',
name: 'checkbox',
},
{
type: 'code',
name: 'code',
},
{
type: 'collapsible',
label: 'Collapsible',
fields: [
{
name: 'textInCollapsible',
type: 'text',
},
],
},
{
type: 'date',
name: 'date',
},
{
type: 'email',
name: 'email',
},
{
type: 'group',
name: 'group',
fields: [
{
name: 'textInGroup',
type: 'text',
},
],
},
{
type: 'number',
name: 'number',
},
{
type: 'point',
name: 'point',
},
{
type: 'radio',
name: 'radio',
options: [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
],
},
{
type: 'relationship',
name: 'relationship',
relationTo: draftCollectionSlug,
},
{
name: 'richtext',
type: 'richText',
},
{
name: 'richtextWithCustomDiff',
type: 'richText',
admin: {
components: {
Diff: './elements/RichTextDiffComponent/index.js#RichTextDiffComponent',
},
},
},
{
fields: [
{
name: 'textInRow',
type: 'text',
},
],
type: 'row',
},
{
name: 'select',
type: 'select',
options: [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
],
},
{
type: 'tabs',
tabs: [
{
name: 'namedTab1',
fields: [
{
name: 'textInNamedTab1',
type: 'text',
},
],
},
{
label: 'Unnamed Tab 2',
fields: [
{
name: 'textInUnnamedTab2',
type: 'text',
},
],
},
],
},
{
name: 'text',
type: 'text',
},
{
name: 'textArea',
type: 'textarea',
},
{
name: 'upload',
relationTo: 'media',
type: 'upload',
},
],
versions: {
maxPerDoc: 35,
},
}

View File

@@ -0,0 +1,17 @@
import type { CollectionConfig } from 'payload'
import path from 'path'
import { fileURLToPath } from 'url'
import { mediaCollectionSlug } from '../slugs.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const Media: CollectionConfig = {
fields: [],
slug: mediaCollectionSlug,
upload: {
staticDir: path.resolve(dirname, './uploads'),
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -5,10 +5,12 @@ const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import AutosavePosts from './collections/Autosave.js'
import CustomIDs from './collections/CustomIDs.js'
import { Diff } from './collections/Diff.js'
import DisablePublish from './collections/DisablePublish.js'
import DraftPosts from './collections/Drafts.js'
import DraftWithMax from './collections/DraftsWithMax.js'
import LocalizedPosts from './collections/Localized.js'
import { Media } from './collections/Media.js'
import Posts from './collections/Posts.js'
import VersionPosts from './collections/Versions.js'
import AutosaveGlobal from './globals/Autosave.js'
@@ -35,6 +37,8 @@ export default buildConfigWithDefaults({
LocalizedPosts,
VersionPosts,
CustomIDs,
Diff,
Media,
],
globals: [AutosaveGlobal, DraftGlobal, DraftWithMaxGlobal, DisablePublishGlobal, LocalizedGlobal],
indexSortableFields: true,

View File

@@ -30,7 +30,7 @@ import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config } from './payload-types.js'
import type { Config, Diff } from './payload-types.js'
import {
changeLocale,
@@ -52,6 +52,7 @@ import {
autosaveCollectionSlug,
autoSaveGlobalSlug,
customIDSlug,
diffCollectionSlug,
disablePublishGlobalSlug,
disablePublishSlug,
draftCollectionSlug,
@@ -785,6 +786,8 @@ describe('Versions', () => {
describe('Versions diff view', () => {
let postID: string
let versionID: string
let diffID: string
let versionDiffID: string
beforeAll(() => {
url = new AdminUrlUtil(serverURL, draftCollectionSlug)
@@ -826,20 +829,47 @@ describe('Versions', () => {
})
versionID = versions.docs[0].id
const diffDoc = (
await payload.find({
collection: diffCollectionSlug,
})
).docs[0] as Diff
diffID = diffDoc.id
const versionDiff = (
await payload.findVersions({
collection: diffCollectionSlug,
where: {
parent: { equals: diffID },
},
})
).docs[0] as Diff
versionDiffID = versionDiff.id
})
test('should render diff', async () => {
async function navigateToVersionDiff() {
const versionURL = `${serverURL}/admin/collections/${draftCollectionSlug}/${postID}/versions/${versionID}`
await page.goto(versionURL)
await page.waitForURL(versionURL)
await expect(page.locator('.render-field-diffs').first()).toBeVisible()
}
async function navigateToVersionFieldsDiff() {
const versionURL = `${serverURL}/admin/collections/${diffCollectionSlug}/${diffID}/versions/${versionDiffID}`
await page.goto(versionURL)
await page.waitForURL(versionURL)
await expect(page.locator('.render-field-diffs').first()).toBeVisible()
}
test('should render diff', async () => {
await navigateToVersionDiff()
})
test('should render diff for nested fields', async () => {
const versionURL = `${serverURL}/admin/collections/${draftCollectionSlug}/${postID}/versions/${versionID}`
await page.goto(versionURL)
await page.waitForURL(versionURL)
await expect(page.locator('.render-field-diffs').first()).toBeVisible()
await navigateToVersionDiff()
const blocksDiffLabel = page.getByText('Blocks Field', { exact: true })
await expect(blocksDiffLabel).toBeVisible()
@@ -858,10 +888,7 @@ describe('Versions', () => {
})
test('should render diff collapser for nested fields', async () => {
const versionURL = `${serverURL}/admin/collections/${draftCollectionSlug}/${postID}/versions/${versionID}`
await page.goto(versionURL)
await page.waitForURL(versionURL)
await expect(page.locator('.render-field-diffs').first()).toBeVisible()
await navigateToVersionDiff()
const blocksDiffLabel = page.getByText('Blocks Field', { exact: true })
await expect(blocksDiffLabel).toBeVisible()
@@ -908,5 +935,233 @@ describe('Versions', () => {
// Expect collapser content to be visible
await expect(diffCollapserContent).toBeVisible()
})
test('correctly renders diff for array fields', async () => {
await navigateToVersionFieldsDiff()
const textInArray = page.locator('[data-field-path="array.0.textInArray"]')
await expect(textInArray.locator('tr').nth(1).locator('td').nth(1)).toHaveText('textInArray')
await expect(textInArray.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInArray2')
})
test('correctly renders diff for block fields', async () => {
await navigateToVersionFieldsDiff()
const textInBlock = page.locator('[data-field-path="blocks.0.textInBlock"]')
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText('textInBlock')
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInBlock2')
})
test('correctly renders diff for checkbox fields', async () => {
await navigateToVersionFieldsDiff()
const checkbox = page.locator('[data-field-path="checkbox"]')
await expect(checkbox.locator('tr').nth(1).locator('td').nth(1)).toHaveText('true')
await expect(checkbox.locator('tr').nth(1).locator('td').nth(3)).toHaveText('false')
})
test('correctly renders diff for code fields', async () => {
await navigateToVersionFieldsDiff()
const code = page.locator('[data-field-path="code"]')
await expect(code.locator('tr').nth(1).locator('td').nth(1)).toHaveText('code')
await expect(code.locator('tr').nth(1).locator('td').nth(3)).toHaveText('code2')
})
test('correctly renders diff for collapsible fields', async () => {
await navigateToVersionFieldsDiff()
const collapsible = page.locator('[data-field-path="textInCollapsible"]')
await expect(collapsible.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'textInCollapsible',
)
await expect(collapsible.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'textInCollapsible2',
)
})
test('correctly renders diff for date fields', async () => {
await navigateToVersionFieldsDiff()
const date = page.locator('[data-field-path="date"]')
await expect(date.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'2021-01-01T00:00:00.000Z',
)
await expect(date.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'2023-01-01T00:00:00.000Z',
)
})
test('correctly renders diff for email fields', async () => {
await navigateToVersionFieldsDiff()
const email = page.locator('[data-field-path="email"]')
await expect(email.locator('tr').nth(1).locator('td').nth(1)).toHaveText('email@email.com')
await expect(email.locator('tr').nth(1).locator('td').nth(3)).toHaveText('email2@email.com')
})
test('correctly renders diff for group fields', async () => {
await navigateToVersionFieldsDiff()
const group = page.locator('[data-field-path="group.textInGroup"]')
await expect(group.locator('tr').nth(1).locator('td').nth(1)).toHaveText('textInGroup')
await expect(group.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInGroup2')
})
test('correctly renders diff for number fields', async () => {
await navigateToVersionFieldsDiff()
const number = page.locator('[data-field-path="number"]')
await expect(number.locator('tr').nth(1).locator('td').nth(1)).toHaveText('1')
await expect(number.locator('tr').nth(1).locator('td').nth(3)).toHaveText('2')
})
test('correctly renders diff for point fields', async () => {
await navigateToVersionFieldsDiff()
const point = page.locator('[data-field-path="point"]')
await expect(point.locator('tr').nth(3).locator('td').nth(1)).toHaveText('2')
await expect(point.locator('tr').nth(3).locator('td').nth(3)).toHaveText('3')
})
test('correctly renders diff for radio fields', async () => {
await navigateToVersionFieldsDiff()
const radio = page.locator('[data-field-path="radio"]')
await expect(radio.locator('tr').nth(1).locator('td').nth(1)).toHaveText('Option 1')
await expect(radio.locator('tr').nth(1).locator('td').nth(3)).toHaveText('Option 2')
})
test('correctly renders diff for relationship fields', async () => {
await navigateToVersionFieldsDiff()
const relationship = page.locator('[data-field-path="relationship"]')
const draftDocs = await payload.find({
collection: 'draft-posts',
sort: 'createdAt',
limit: 3,
})
await expect(relationship.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
String(draftDocs?.docs?.[1]?.id),
)
await expect(relationship.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
String(draftDocs?.docs?.[2]?.id),
)
})
test('correctly renders diff for richtext fields', async () => {
await navigateToVersionFieldsDiff()
const richtext = page.locator('[data-field-path="richtext"]')
await expect(richtext.locator('tr').nth(16).locator('td').nth(1)).toHaveText(
'"text": "richtext",',
)
await expect(richtext.locator('tr').nth(16).locator('td').nth(3)).toHaveText(
'"text": "richtext2",',
)
})
test('correctly renders diff for richtext fields with custom Diff component', async () => {
await navigateToVersionFieldsDiff()
const richtextWithCustomDiff = page.locator('[data-field-path="richtextWithCustomDiff"]')
await expect(richtextWithCustomDiff.locator('p')).toHaveText('Test')
})
test('correctly renders diff for row fields', async () => {
await navigateToVersionFieldsDiff()
const textInRow = page.locator('[data-field-path="textInRow"]')
await expect(textInRow.locator('tr').nth(1).locator('td').nth(1)).toHaveText('textInRow')
await expect(textInRow.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInRow2')
})
test('correctly renders diff for select fields', async () => {
await navigateToVersionFieldsDiff()
const select = page.locator('[data-field-path="select"]')
await expect(select.locator('tr').nth(1).locator('td').nth(1)).toHaveText('Option 1')
await expect(select.locator('tr').nth(1).locator('td').nth(3)).toHaveText('Option 2')
})
test('correctly renders diff for named tabs', async () => {
await navigateToVersionFieldsDiff()
const textInNamedTab1 = page.locator('[data-field-path="namedTab1.textInNamedTab1"]')
await expect(textInNamedTab1.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'textInNamedTab1',
)
await expect(textInNamedTab1.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'textInNamedTab12',
)
})
test('correctly renders diff for unnamed tabs', async () => {
await navigateToVersionFieldsDiff()
const textInUnamedTab2 = page.locator('[data-field-path="textInUnnamedTab2"]')
await expect(textInUnamedTab2.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'textInUnnamedTab2',
)
await expect(textInUnamedTab2.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'textInUnnamedTab22',
)
})
test('correctly renders diff for text fields', async () => {
await navigateToVersionFieldsDiff()
const text = page.locator('[data-field-path="text"]')
await expect(text.locator('tr').nth(1).locator('td').nth(1)).toHaveText('text')
await expect(text.locator('tr').nth(1).locator('td').nth(3)).toHaveText('text2')
})
test('correctly renders diff for textArea fields', async () => {
await navigateToVersionFieldsDiff()
const textArea = page.locator('[data-field-path="textArea"]')
await expect(textArea.locator('tr').nth(1).locator('td').nth(1)).toHaveText('textArea')
await expect(textArea.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textArea2')
})
test('correctly renders diff for upload fields', async () => {
await navigateToVersionFieldsDiff()
const upload = page.locator('[data-field-path="upload"]')
const uploadDocs = await payload.find({
collection: 'media',
sort: 'createdAt',
limit: 2,
})
await expect(upload.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
String(uploadDocs?.docs?.[0]?.id),
)
await expect(upload.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
String(uploadDocs?.docs?.[1]?.id),
)
})
})
})

View File

@@ -0,0 +1,5 @@
import type { RichTextFieldDiffServerComponent } from 'payload'
export const RichTextDiffComponent: RichTextFieldDiffServerComponent = () => {
return <p>Test</p>
}

BIN
test/versions/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
test/versions/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -19,6 +19,8 @@ export interface Config {
'localized-posts': LocalizedPost;
'version-posts': VersionPost;
'custom-ids': CustomId;
diff: Diff;
media: Media;
users: User;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument;
@@ -35,6 +37,8 @@ export interface Config {
'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;
'version-posts': VersionPostsSelect<false> | VersionPostsSelect<true>;
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
diff: DiffSelect<false> | DiffSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@@ -208,6 +212,102 @@ export interface CustomId {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "diff".
*/
export interface Diff {
id: string;
array?:
| {
textInArray?: string | null;
id?: string | null;
}[]
| null;
blocks?:
| {
textInBlock?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'TextBlock';
}[]
| null;
checkbox?: boolean | null;
code?: string | null;
textInCollapsible?: string | null;
date?: string | null;
email?: string | null;
group?: {
textInGroup?: string | null;
};
number?: number | null;
/**
* @minItems 2
* @maxItems 2
*/
point?: [number, number] | null;
radio?: ('option1' | 'option2') | null;
relationship?: (string | null) | DraftPost;
richtext?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
richtextWithCustomDiff?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
textInRow?: string | null;
select?: ('option1' | 'option2') | null;
namedTab1?: {
textInNamedTab1?: string | null;
};
textInUnnamedTab2?: string | null;
text?: string | null;
textArea?: string | null;
upload?: (string | null) | Media;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -356,6 +456,14 @@ export interface PayloadLockedDocument {
relationTo: 'custom-ids';
value: string | CustomId;
} | null)
| ({
relationTo: 'diff';
value: string | Diff;
} | null)
| ({
relationTo: 'media';
value: string | Media;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -522,6 +630,75 @@ export interface CustomIdsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "diff_select".
*/
export interface DiffSelect<T extends boolean = true> {
array?:
| T
| {
textInArray?: T;
id?: T;
};
blocks?:
| T
| {
TextBlock?:
| T
| {
textInBlock?: T;
id?: T;
blockName?: T;
};
};
checkbox?: T;
code?: T;
textInCollapsible?: T;
date?: T;
email?: T;
group?:
| T
| {
textInGroup?: T;
};
number?: T;
point?: T;
radio?: T;
relationship?: T;
richtext?: T;
richtextWithCustomDiff?: T;
textInRow?: T;
select?: T;
namedTab1?:
| T
| {
textInNamedTab1?: T;
};
textInUnnamedTab2?: T;
text?: T;
textArea?: T;
upload?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select".
*/
export interface MediaSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".

View File

@@ -1,11 +1,17 @@
import { type Payload } from 'payload'
import path from 'path'
import { getFileByPath, type Payload } from 'payload'
import { fileURLToPath } from 'url'
import type { DraftPost } from './payload-types.js'
import { devUser } from '../credentials.js'
import { executePromises } from '../helpers/executePromises.js'
import { titleToDelete } from './shared.js'
import { draftCollectionSlug } from './slugs.js'
import { diffCollectionSlug, draftCollectionSlug, mediaCollectionSlug } from './slugs.js'
import { textToLexicalJSON } from './textToLexicalJSON.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export async function seed(_payload: Payload, parallel: boolean = false) {
const blocksField: DraftPost['blocksField'] = [
@@ -16,6 +22,24 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
},
]
const imageFilePath = path.resolve(dirname, './image.jpg')
const imageFile = await getFileByPath(imageFilePath)
const { id: uploadedImage } = await _payload.create({
collection: mediaCollectionSlug,
data: {},
file: imageFile,
})
const imageFilePath2 = path.resolve(dirname, './image.png')
const imageFile2 = await getFileByPath(imageFilePath2)
const { id: uploadedImage2 } = await _payload.create({
collection: mediaCollectionSlug,
data: {},
file: imageFile2,
})
await executePromises(
[
() =>
@@ -70,7 +94,7 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
})
}
await _payload.create({
const draft2 = await _payload.create({
collection: draftCollectionSlug,
data: {
_status: 'published',
@@ -95,4 +119,87 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
overrideAccess: true,
draft: true,
})
const diffDoc = await _payload.create({
collection: diffCollectionSlug,
data: {
array: [
{
textInArray: 'textInArray',
},
],
blocks: [
{
blockType: 'TextBlock',
textInBlock: 'textInBlock',
},
],
checkbox: true,
code: 'code',
date: '2021-01-01T00:00:00.000Z',
email: 'email@email.com',
group: {
textInGroup: 'textInGroup',
},
namedTab1: {
textInNamedTab1: 'textInNamedTab1',
},
number: 1,
point: [1, 2],
radio: 'option1',
relationship: manyDraftsID,
richtext: textToLexicalJSON({ text: 'richtext' }),
richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff' }),
select: 'option1',
text: 'text',
textArea: 'textArea',
textInCollapsible: 'textInCollapsible',
textInRow: 'textInRow',
textInUnnamedTab2: 'textInUnnamedTab2',
upload: uploadedImage,
},
depth: 0,
})
const updatedDiffDoc = await _payload.update({
id: diffDoc.id,
collection: diffCollectionSlug,
data: {
array: [
{
textInArray: 'textInArray2',
},
],
blocks: [
{
blockType: 'TextBlock',
textInBlock: 'textInBlock2',
},
],
checkbox: false,
code: 'code2',
date: '2023-01-01T00:00:00.000Z',
email: 'email2@email.com',
group: {
textInGroup: 'textInGroup2',
},
namedTab1: {
textInNamedTab1: 'textInNamedTab12',
},
number: 2,
point: [1, 3],
radio: 'option2',
relationship: draft2.id,
richtext: textToLexicalJSON({ text: 'richtext2' }),
richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff2' }),
select: 'option2',
text: 'text2',
textArea: 'textArea2',
textInCollapsible: 'textInCollapsible2',
textInRow: 'textInRow2',
textInUnnamedTab2: 'textInUnnamedTab22',
upload: uploadedImage2,
},
depth: 0,
})
}

View File

@@ -7,6 +7,9 @@ export const draftWithMaxCollectionSlug = 'draft-with-max-posts'
export const postCollectionSlug = 'posts'
export const diffCollectionSlug = 'diff'
export const mediaCollectionSlug = 'media'
export const versionCollectionSlug = 'version-posts'
export const disablePublishSlug = 'disable-publish'
@@ -17,6 +20,8 @@ export const collectionSlugs = [
autosaveCollectionSlug,
draftCollectionSlug,
postCollectionSlug,
diffCollectionSlug,
mediaCollectionSlug,
versionCollectionSlug,
]

View File

@@ -0,0 +1,41 @@
import type {
SerializedEditorState,
SerializedParagraphNode,
SerializedTextNode,
} from '@payloadcms/richtext-lexical/lexical'
export function textToLexicalJSON({ text }: { text: string }): any {
const editorJSON: SerializedEditorState = {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
direction: 'ltr',
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text,
type: 'text',
version: 1,
} as SerializedTextNode,
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
type: 'paragraph',
textStyle: '',
version: 1,
} as SerializedParagraphNode,
],
},
}
return editorJSON
}