feat: allows fields to be collapsed in the version view diff (#8054)
## Description Allows some fields to be collapsed in the version diff view. The fields that can be collapsed are the ones which can also be collapsed in the edit view, or that have visual grouping: - `collapsible` - `group` - `array` (and their rows) - `blocks` (and their rows) - `tabs` It also - Fixes incorrect indentation of some fields - Fixes the rendering of localized tabs in the diff view - Fixes locale labels for the group field - Adds a field change count to each collapsible diff (could imagine this being used in other places) - Brings the indentation gutter back to help visualize multiple nesting levels ## Future improvements - Persist collapsed state across page reloads (sessionStorage vs preferences) ## Screenshots ### Without locales  ### With locales  -------------- - [x] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository. ## Type of change <!-- Please delete options that are not relevant. --> - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] I have added tests that prove my fix is effective or that my feature works - [x] Existing test suite passes locally with my changes - [ ] ~I have made corresponding changes to the documentation~
This commit is contained in:
committed by
GitHub
parent
92e6beb050
commit
828b3b71c0
@@ -8,7 +8,7 @@ import React, { useState } from 'react'
|
||||
import type { CompareOption, DefaultVersionsViewProps } from './types.js'
|
||||
|
||||
import { diffComponents } from '../RenderFieldsToDiff/fields/index.js'
|
||||
import RenderFieldsToDiff from '../RenderFieldsToDiff/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'
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@layer payload-default {
|
||||
.diff-collapser {
|
||||
&__toggle-button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
// Align the chevron visually with the label text
|
||||
vertical-align: 1px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
// Add space between label, chevron, and change count
|
||||
margin: 0 calc(var(--base) * 0.25);
|
||||
}
|
||||
|
||||
&__field-change-count {
|
||||
// Reset the font weight of the change count to normal
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__content {
|
||||
[dir='ltr'] & {
|
||||
// Vertical gutter
|
||||
border-left: 3px solid var(--theme-elevation-50);
|
||||
// Center-align the gutter with the chevron
|
||||
margin-left: 3px;
|
||||
// Content indentation
|
||||
padding-left: calc(var(--base) * 0.5);
|
||||
}
|
||||
[dir='rtl'] & {
|
||||
// Vertical gutter
|
||||
border-right: 3px solid var(--theme-elevation-50);
|
||||
// Center-align the gutter with the chevron
|
||||
margin-right: 3px;
|
||||
// Content indentation
|
||||
padding-right: calc(var(--base) * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__content--is-collapsed {
|
||||
// Hide the content when collapsed. We use display: none instead of
|
||||
// conditional rendering to avoid loosing children's collapsed state when
|
||||
// remounting.
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { ChevronIcon, Pill, useTranslation } from '@payloadcms/ui'
|
||||
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Label from '../Label/index.js'
|
||||
import './index.scss'
|
||||
import { countChangedFields, countChangedFieldsInRows } from '../utilities/countChangedFields.js'
|
||||
|
||||
const baseClass = 'diff-collapser'
|
||||
|
||||
type Props =
|
||||
| {
|
||||
// fields collapser
|
||||
children: React.ReactNode
|
||||
comparison: unknown
|
||||
field?: never
|
||||
fields: ClientField[]
|
||||
initCollapsed?: boolean
|
||||
isIterable?: false
|
||||
label: React.ReactNode
|
||||
locales: string[] | undefined
|
||||
version: unknown
|
||||
}
|
||||
| {
|
||||
// iterable collapser
|
||||
children: React.ReactNode
|
||||
comparison?: unknown
|
||||
field: ClientField
|
||||
fields?: never
|
||||
initCollapsed?: boolean
|
||||
isIterable: true
|
||||
label: React.ReactNode
|
||||
locales: string[] | undefined
|
||||
version: unknown
|
||||
}
|
||||
|
||||
export const DiffCollapser: React.FC<Props> = ({
|
||||
children,
|
||||
comparison,
|
||||
field,
|
||||
fields,
|
||||
initCollapsed = false,
|
||||
isIterable = false,
|
||||
label,
|
||||
locales,
|
||||
version,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCollapsed, setIsCollapsed] = useState(initCollapsed)
|
||||
|
||||
let changeCount = 0
|
||||
|
||||
if (isIterable) {
|
||||
if (!fieldIsArrayType(field) && !fieldIsBlockType(field)) {
|
||||
throw new Error(
|
||||
'DiffCollapser: field must be an array or blocks field when isIterable is true',
|
||||
)
|
||||
}
|
||||
const comparisonRows = comparison ?? []
|
||||
const versionRows = version ?? []
|
||||
|
||||
if (!Array.isArray(comparisonRows) || !Array.isArray(versionRows)) {
|
||||
throw new Error(
|
||||
'DiffCollapser: comparison and version must be arrays when isIterable is true',
|
||||
)
|
||||
}
|
||||
|
||||
changeCount = countChangedFieldsInRows({
|
||||
comparisonRows,
|
||||
field,
|
||||
locales,
|
||||
versionRows,
|
||||
})
|
||||
} else {
|
||||
changeCount = countChangedFields({
|
||||
comparison,
|
||||
fields,
|
||||
locales,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
const contentClassNames = [
|
||||
`${baseClass}__content`,
|
||||
isCollapsed && `${baseClass}__content--is-collapsed`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Label>
|
||||
<button
|
||||
aria-label={isCollapsed ? 'Expand' : 'Collapse'}
|
||||
className={`${baseClass}__toggle-button`}
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
type="button"
|
||||
>
|
||||
<ChevronIcon direction={isCollapsed ? 'right' : 'down'} />
|
||||
</button>
|
||||
<span className={`${baseClass}__label`}>{label}</span>
|
||||
{changeCount > 0 && (
|
||||
<Pill className={`${baseClass}__field-change-count`} pillStyle="light-gray" size="small">
|
||||
{t('version:changedFieldsCount', { count: changeCount })}
|
||||
</Pill>
|
||||
)}
|
||||
</Label>
|
||||
<div className={contentClassNames}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import type { DiffComponentProps } from '../types.js'
|
||||
|
||||
import { DiffCollapser } from '../../DiffCollapser/index.js'
|
||||
import { RenderFieldsToDiff } from '../../index.js'
|
||||
|
||||
const baseClass = 'collapsible-diff'
|
||||
|
||||
export const Collapsible: React.FC<DiffComponentProps> = ({
|
||||
comparison,
|
||||
diffComponents,
|
||||
field,
|
||||
fieldPermissions,
|
||||
fields,
|
||||
i18n,
|
||||
locales,
|
||||
version,
|
||||
}) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<DiffCollapser
|
||||
comparison={comparison}
|
||||
fields={fields}
|
||||
label={
|
||||
'label' in field &&
|
||||
field.label &&
|
||||
typeof field.label !== 'function' && <span>{getTranslation(field.label, i18n)}</span>
|
||||
}
|
||||
locales={locales}
|
||||
version={version}
|
||||
>
|
||||
<RenderFieldsToDiff
|
||||
comparison={comparison}
|
||||
diffComponents={diffComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={fields}
|
||||
i18n={i18n}
|
||||
locales={locales}
|
||||
version={version}
|
||||
/>
|
||||
</DiffCollapser>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@layer payload-default {
|
||||
.group-diff {
|
||||
&__locale-label {
|
||||
background: var(--theme-elevation-100);
|
||||
padding: calc(var(--base) * 0.25);
|
||||
[dir='ltr'] & {
|
||||
margin-right: calc(var(--base) * 0.25);
|
||||
}
|
||||
[dir='rtl'] & {
|
||||
margin-left: calc(var(--base) * 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import type { DiffComponentProps } from '../types.js'
|
||||
|
||||
import { DiffCollapser } from '../../DiffCollapser/index.js'
|
||||
import { RenderFieldsToDiff } from '../../index.js'
|
||||
|
||||
const baseClass = 'group-diff'
|
||||
|
||||
export const Group: React.FC<DiffComponentProps> = ({
|
||||
comparison,
|
||||
diffComponents,
|
||||
field,
|
||||
fieldPermissions,
|
||||
fields,
|
||||
i18n,
|
||||
locale,
|
||||
locales,
|
||||
version,
|
||||
}) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<DiffCollapser
|
||||
comparison={comparison}
|
||||
fields={fields}
|
||||
label={
|
||||
'label' in field &&
|
||||
field.label &&
|
||||
typeof field.label !== 'function' && (
|
||||
<span>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{getTranslation(field.label, i18n)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
locales={locales}
|
||||
version={version}
|
||||
>
|
||||
<RenderFieldsToDiff
|
||||
comparison={comparison}
|
||||
diffComponents={diffComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={fields}
|
||||
i18n={i18n}
|
||||
locales={locales}
|
||||
version={version}
|
||||
/>
|
||||
</DiffCollapser>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
@layer payload-default {
|
||||
.iterable-diff {
|
||||
margin-bottom: calc(var(--base) * 2);
|
||||
|
||||
&__locale-label {
|
||||
background: var(--theme-elevation-100);
|
||||
padding: calc(var(--base) * 0.25);
|
||||
@@ -14,16 +12,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
margin: calc(var(--base) * 0.5);
|
||||
[dir='ltr'] & {
|
||||
padding-left: calc(var(--base) * 0.5);
|
||||
// border-left: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
[dir='rtl'] & {
|
||||
padding-right: calc(var(--base) * 0.5);
|
||||
// border-right: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
// Space between each row
|
||||
&__row:not(:first-of-type) {
|
||||
margin-top: calc(var(--base) * 0.5);
|
||||
}
|
||||
|
||||
&__no-rows {
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { getUniqueListBy } from 'payload/shared'
|
||||
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import type { DiffComponentProps } from '../types.js'
|
||||
|
||||
import RenderFieldsToDiff from '../../index.js'
|
||||
import Label from '../../Label/index.js'
|
||||
import { DiffCollapser } from '../../DiffCollapser/index.js'
|
||||
import './index.scss'
|
||||
import { RenderFieldsToDiff } from '../../index.js'
|
||||
import { getFieldsForRowComparison } from '../../utilities/getFieldsForRowComparison.js'
|
||||
|
||||
const baseClass = 'iterable-diff'
|
||||
|
||||
const Iterable: React.FC<DiffComponentProps> = ({
|
||||
export const Iterable: React.FC<DiffComponentProps> = ({
|
||||
comparison,
|
||||
diffComponents,
|
||||
field,
|
||||
@@ -27,88 +28,79 @@ const Iterable: React.FC<DiffComponentProps> = ({
|
||||
const comparisonRowCount = Array.isArray(comparison) ? comparison.length : 0
|
||||
const maxRows = Math.max(versionRowCount, comparisonRowCount)
|
||||
|
||||
if (!fieldIsArrayType(field) && !fieldIsBlockType(field)) {
|
||||
throw new Error(`Expected field to be an array or blocks type but got: ${field.type}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{'label' in field && field.label && typeof field.label !== 'function' && (
|
||||
<Label>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{getTranslation(field.label, i18n)}
|
||||
</Label>
|
||||
)}
|
||||
{maxRows > 0 && (
|
||||
<React.Fragment>
|
||||
{Array.from(Array(maxRows).keys()).map((row, i) => {
|
||||
const versionRow = version?.[i] || {}
|
||||
const comparisonRow = comparison?.[i] || {}
|
||||
<DiffCollapser
|
||||
comparison={comparison}
|
||||
field={field}
|
||||
isIterable
|
||||
label={
|
||||
'label' in field &&
|
||||
field.label &&
|
||||
typeof field.label !== 'function' && (
|
||||
<span>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{getTranslation(field.label, i18n)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
locales={locales}
|
||||
version={version}
|
||||
>
|
||||
{maxRows > 0 && (
|
||||
<div className={`${baseClass}__rows`}>
|
||||
{Array.from(Array(maxRows).keys()).map((row, i) => {
|
||||
const versionRow = version?.[i] || {}
|
||||
const comparisonRow = comparison?.[i] || {}
|
||||
|
||||
let fields: ClientField[] = []
|
||||
const fields: ClientField[] = getFieldsForRowComparison({
|
||||
comparisonRow,
|
||||
field,
|
||||
versionRow,
|
||||
})
|
||||
|
||||
if (field.type === 'array' && 'fields' in field) {
|
||||
fields = field.fields
|
||||
}
|
||||
const rowNumber = String(i + 1).padStart(2, '0')
|
||||
const rowLabel = fieldIsArrayType(field) ? `Item ${rowNumber}` : `Block ${rowNumber}`
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
fields = [
|
||||
// {
|
||||
// name: 'blockType',
|
||||
// label: i18n.t('fields:blockType'),
|
||||
// type: 'text',
|
||||
// },
|
||||
]
|
||||
|
||||
if (versionRow?.blockType === comparisonRow?.blockType) {
|
||||
const matchedBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
|
||||
fields = [...fields, ...matchedBlock.fields]
|
||||
} else {
|
||||
const matchedVersionBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
|
||||
const matchedComparisonBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === comparisonRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
|
||||
fields = getUniqueListBy<ClientField>(
|
||||
[...fields, ...matchedVersionBlock.fields, ...matchedComparisonBlock.fields],
|
||||
'name',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__wrap`} key={i}>
|
||||
<RenderFieldsToDiff
|
||||
comparison={comparisonRow}
|
||||
diffComponents={diffComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={fields}
|
||||
i18n={i18n}
|
||||
locales={locales}
|
||||
version={versionRow}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{maxRows === 0 && (
|
||||
<div className={`${baseClass}__no-rows`}>
|
||||
{i18n.t('version:noRowsFound', {
|
||||
label:
|
||||
'labels' in field && field.labels?.plural
|
||||
? getTranslation(field.labels.plural, i18n)
|
||||
: i18n.t('general:rows'),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div className={`${baseClass}__row`} key={i}>
|
||||
<DiffCollapser
|
||||
comparison={comparisonRow}
|
||||
fields={fields}
|
||||
label={rowLabel}
|
||||
locales={locales}
|
||||
version={versionRow}
|
||||
>
|
||||
<RenderFieldsToDiff
|
||||
comparison={comparisonRow}
|
||||
diffComponents={diffComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={fields}
|
||||
i18n={i18n}
|
||||
locales={locales}
|
||||
version={versionRow}
|
||||
/>
|
||||
</DiffCollapser>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{maxRows === 0 && (
|
||||
<div className={`${baseClass}__no-rows`}>
|
||||
{i18n.t('version:noRowsFound', {
|
||||
label:
|
||||
'labels' in field && field.labels?.plural
|
||||
? getTranslation(field.labels.plural, i18n)
|
||||
: i18n.t('general:rows'),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</DiffCollapser>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Iterable
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
@layer payload-default {
|
||||
.nested-diff {
|
||||
&__wrap--gutter {
|
||||
[dir='ltr'] & {
|
||||
padding-left: calc(var(--base) * 0.25);
|
||||
// border-left: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
[dir='rtl'] & {
|
||||
padding-right: calc(var(--base) * 0.25);
|
||||
// border-right: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import type { DiffComponentProps } from '../types.js'
|
||||
|
||||
import RenderFieldsToDiff from '../../index.js'
|
||||
import Label from '../../Label/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'nested-diff'
|
||||
|
||||
const Nested: React.FC<DiffComponentProps> = ({
|
||||
comparison,
|
||||
diffComponents,
|
||||
disableGutter = false,
|
||||
field,
|
||||
fieldPermissions,
|
||||
fields,
|
||||
i18n,
|
||||
locale,
|
||||
locales,
|
||||
version,
|
||||
}) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{'label' in field && field.label && typeof field.label !== 'function' && (
|
||||
<Label>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{getTranslation(field.label, i18n)}
|
||||
</Label>
|
||||
)}
|
||||
<div
|
||||
className={[`${baseClass}__wrap`, !disableGutter && `${baseClass}__wrap--gutter`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<RenderFieldsToDiff
|
||||
comparison={comparison}
|
||||
diffComponents={diffComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={fields}
|
||||
i18n={i18n}
|
||||
locales={locales}
|
||||
version={version}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nested
|
||||
@@ -99,7 +99,7 @@ const generateLabelFromValue = (
|
||||
return valueToReturn
|
||||
}
|
||||
|
||||
const Relationship: React.FC<DiffComponentProps<RelationshipFieldClient>> = ({
|
||||
export const Relationship: React.FC<DiffComponentProps<RelationshipFieldClient>> = ({
|
||||
comparison,
|
||||
field,
|
||||
i18n,
|
||||
@@ -159,5 +159,3 @@ const Relationship: React.FC<DiffComponentProps<RelationshipFieldClient>> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Relationship
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import type { DiffComponentProps } from '../types.js'
|
||||
|
||||
import { RenderFieldsToDiff } from '../../index.js'
|
||||
import Label from '../../Label/index.js'
|
||||
|
||||
const baseClass = 'row-diff'
|
||||
|
||||
export const Row: React.FC<DiffComponentProps> = ({
|
||||
comparison,
|
||||
diffComponents,
|
||||
field,
|
||||
fieldPermissions,
|
||||
fields,
|
||||
i18n,
|
||||
locales,
|
||||
version,
|
||||
}) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const getTranslatedOptions = (
|
||||
return typeof options === 'string' ? options : getTranslation(options.label, i18n)
|
||||
}
|
||||
|
||||
const Select: React.FC<DiffComponentProps<SelectFieldClient>> = ({
|
||||
export const Select: React.FC<DiffComponentProps<SelectFieldClient>> = ({
|
||||
comparison,
|
||||
diffMethod,
|
||||
field,
|
||||
@@ -87,5 +87,3 @@ const Select: React.FC<DiffComponentProps<SelectFieldClient>> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Select
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
@layer payload-default {
|
||||
.tabs-diff {
|
||||
// Space between each tab or tab locale
|
||||
&__tab:not(:first-of-type),
|
||||
&__tab-locale:not(:first-of-type) {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
|
||||
&__locale-label {
|
||||
background: var(--theme-elevation-100);
|
||||
padding: calc(var(--base) * 0.25);
|
||||
[dir='ltr'] & {
|
||||
margin-right: calc(var(--base) * 0.25);
|
||||
}
|
||||
[dir='rtl'] & {
|
||||
margin-left: calc(var(--base) * 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,101 @@
|
||||
'use client'
|
||||
import type { TabsFieldClient } from 'payload'
|
||||
import type { ClientTab, TabsFieldClient } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import type { DiffComponentProps } from '../types.js'
|
||||
|
||||
import RenderFieldsToDiff from '../../index.js'
|
||||
import Nested from '../Nested/index.js'
|
||||
import { DiffCollapser } from '../../DiffCollapser/index.js'
|
||||
import { RenderFieldsToDiff } from '../../index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'tabs-diff'
|
||||
|
||||
const Tabs: React.FC<DiffComponentProps<TabsFieldClient>> = ({
|
||||
comparison,
|
||||
diffComponents,
|
||||
field,
|
||||
fieldPermissions,
|
||||
i18n,
|
||||
locale,
|
||||
locales,
|
||||
version,
|
||||
}) => {
|
||||
export const Tabs: React.FC<DiffComponentProps<TabsFieldClient>> = (props) => {
|
||||
const { comparison, field, locales, version } = props
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
{field.tabs.map((tab, i) => {
|
||||
if ('name' in tab) {
|
||||
return (
|
||||
<Nested
|
||||
comparison={comparison?.[tab.name]}
|
||||
diffComponents={diffComponents}
|
||||
field={field}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={tab.fields}
|
||||
i18n={i18n}
|
||||
key={i}
|
||||
locale={locale}
|
||||
locales={locales}
|
||||
version={version?.[tab.name]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderFieldsToDiff
|
||||
comparison={comparison}
|
||||
diffComponents={diffComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={tab.fields}
|
||||
i18n={i18n}
|
||||
key={i}
|
||||
locales={locales}
|
||||
version={version}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{field.tabs.map((tab, i) => {
|
||||
return (
|
||||
<div className={`${baseClass}__tab`} key={i}>
|
||||
{(() => {
|
||||
if ('name' in tab && locales && tab.localized) {
|
||||
// Named localized tab
|
||||
return locales.map((locale, index) => {
|
||||
const localizedTabProps = {
|
||||
...props,
|
||||
comparison: comparison?.[tab.name]?.[locale],
|
||||
version: version?.[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} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
} else if ('name' in tab && tab.name) {
|
||||
// Named tab
|
||||
const namedTabProps = {
|
||||
...props,
|
||||
comparison: comparison?.[tab.name],
|
||||
version: version?.[tab.name],
|
||||
}
|
||||
return <Tab key={i} {...namedTabProps} tab={tab} />
|
||||
} else {
|
||||
// Unnamed tab
|
||||
return <Tab key={i} {...props} tab={tab} />
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tabs
|
||||
type TabProps = {
|
||||
tab: ClientTab
|
||||
} & DiffComponentProps<TabsFieldClient>
|
||||
|
||||
const Tab: React.FC<TabProps> = ({
|
||||
comparison,
|
||||
diffComponents,
|
||||
fieldPermissions,
|
||||
i18n,
|
||||
locale,
|
||||
locales,
|
||||
tab,
|
||||
version,
|
||||
}) => {
|
||||
return (
|
||||
<DiffCollapser
|
||||
comparison={comparison}
|
||||
fields={tab.fields}
|
||||
label={
|
||||
'label' in tab &&
|
||||
tab.label &&
|
||||
typeof tab.label !== 'function' && (
|
||||
<span>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{getTranslation(tab.label, i18n)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
locales={locales}
|
||||
version={version}
|
||||
>
|
||||
<RenderFieldsToDiff
|
||||
comparison={comparison}
|
||||
diffComponents={diffComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={tab.fields}
|
||||
i18n={i18n}
|
||||
locales={locales}
|
||||
version={version}
|
||||
/>
|
||||
</DiffCollapser>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'text-diff'
|
||||
|
||||
const Text: React.FC<DiffComponentProps<TextFieldClient>> = ({
|
||||
export const Text: React.FC<DiffComponentProps<TextFieldClient>> = ({
|
||||
comparison,
|
||||
diffMethod,
|
||||
field,
|
||||
@@ -58,5 +58,3 @@ const Text: React.FC<DiffComponentProps<TextFieldClient>> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Text
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import Iterable from './Iterable/index.js'
|
||||
import Nested from './Nested/index.js'
|
||||
import Relationship from './Relationship/index.js'
|
||||
import Select from './Select/index.js'
|
||||
import Tabs from './Tabs/index.js'
|
||||
import Text from './Text/index.js'
|
||||
import { Collapsible } from './Collapsible/index.js'
|
||||
import { Group } from './Group/index.js'
|
||||
import { Iterable } from './Iterable/index.js'
|
||||
import { Relationship } from './Relationship/index.js'
|
||||
import { Row } from './Row/index.js'
|
||||
import { Select } from './Select/index.js'
|
||||
import { Tabs } from './Tabs/index.js'
|
||||
import { Text } from './Text/index.js'
|
||||
|
||||
export const diffComponents = {
|
||||
array: Iterable,
|
||||
blocks: Iterable,
|
||||
checkbox: Text,
|
||||
code: Text,
|
||||
collapsible: Nested,
|
||||
collapsible: Collapsible,
|
||||
date: Text,
|
||||
email: Text,
|
||||
group: Nested,
|
||||
group: Group,
|
||||
json: Text,
|
||||
number: Text,
|
||||
point: Text,
|
||||
radio: Select,
|
||||
relationship: Relationship,
|
||||
richText: Text,
|
||||
row: Nested,
|
||||
row: Row,
|
||||
select: Select,
|
||||
tabs: Tabs,
|
||||
text: Text,
|
||||
|
||||
@@ -9,7 +9,6 @@ export type DiffComponentProps<TField extends ClientField = ClientField> = {
|
||||
readonly comparison: any
|
||||
readonly diffComponents: DiffComponents
|
||||
readonly diffMethod?: DiffMethod
|
||||
readonly disableGutter?: boolean
|
||||
readonly field: TField
|
||||
readonly fieldPermissions?:
|
||||
| {
|
||||
|
||||
@@ -8,12 +8,11 @@ import type { diffComponents as _diffComponents } from './fields/index.js'
|
||||
import type { FieldDiffProps, Props } from './types.js'
|
||||
|
||||
import { diffMethods } from './fields/diffMethods.js'
|
||||
import Nested from './fields/Nested/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'render-field-diffs'
|
||||
|
||||
const RenderFieldsToDiff: React.FC<Props> = ({
|
||||
export const RenderFieldsToDiff: React.FC<Props> = ({
|
||||
comparison,
|
||||
diffComponents: __diffComponents,
|
||||
fieldPermissions,
|
||||
@@ -128,13 +127,13 @@ const RenderFieldsToDiff: React.FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
// At this point, we are dealing with a `row`, etc
|
||||
// At this point, we are dealing with a field with subfields but no
|
||||
// nested data, eg. row, collapsible, etc.
|
||||
if ('fields' in field) {
|
||||
return (
|
||||
<Nested
|
||||
<Component
|
||||
comparison={comparison}
|
||||
diffComponents={diffComponents}
|
||||
disableGutter
|
||||
field={field}
|
||||
fieldPermissions={fieldPermissions}
|
||||
fields={field.fields}
|
||||
@@ -152,5 +151,3 @@ const RenderFieldsToDiff: React.FC<Props> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderFieldsToDiff
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
import { countChangedFields, countChangedFieldsInRows } from './countChangedFields.js'
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
describe('countChangedFields', () => {
|
||||
// locales can be undefined when not configured in payload.config.js
|
||||
const locales = undefined
|
||||
it('should return 0 when no fields have changed', () => {
|
||||
const fields: ClientField[] = [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'number' },
|
||||
]
|
||||
const comparison = { a: 'original', b: 123 }
|
||||
const version = { a: 'original', b: 123 }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
|
||||
it('should count simple changed fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'number' },
|
||||
]
|
||||
const comparison = { a: 'original', b: 123 }
|
||||
const version = { a: 'changed', b: 123 }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
it('should count previously undefined fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'number' },
|
||||
]
|
||||
const comparison = {}
|
||||
const version = { a: 'new', b: 123 }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should not count the id field because it is not displayed in the version view', () => {
|
||||
const fields: ClientField[] = [
|
||||
{ name: 'id', type: 'text' },
|
||||
{ name: 'a', type: 'text' },
|
||||
]
|
||||
const comparison = { id: 'original', a: 'original' }
|
||||
const version = { id: 'changed', a: 'original' }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
|
||||
it('should count changed fields inside collapsible fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'A collapsible field',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = { a: 'original', b: 'original', c: 'original' }
|
||||
const version = { a: 'changed', b: 'changed', c: 'original' }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should count changed fields inside row fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = { a: 'original', b: 'original', c: 'original' }
|
||||
const version = { a: 'changed', b: 'changed', c: 'original' }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should count changed fields inside group fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'group',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = { group: { a: 'original', b: 'original', c: 'original' } }
|
||||
const version = { group: { a: 'changed', b: 'changed', c: 'original' } }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should count changed fields inside unnamed tabs ', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Unnamed tab',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = { a: 'original', b: 'original', c: 'original' }
|
||||
const version = { a: 'changed', b: 'changed', c: 'original' }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should count changed fields inside named tabs ', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'namedTab',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = { namedTab: { a: 'original', b: 'original', c: 'original' } }
|
||||
const version = { namedTab: { a: 'changed', b: 'changed', c: 'original' } }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should ignore UI fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{ name: 'a', type: 'text' },
|
||||
{
|
||||
name: 'b',
|
||||
type: 'ui',
|
||||
admin: {},
|
||||
},
|
||||
]
|
||||
const comparison = { a: 'original', b: 'original' }
|
||||
const version = { a: 'original', b: 'changed' }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
|
||||
it('should count changed fields inside array fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
name: 'arrayField',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = {
|
||||
arrayField: [
|
||||
{ a: 'original', b: 'original', c: 'original' },
|
||||
{ a: 'original', b: 'original' },
|
||||
],
|
||||
}
|
||||
const version = {
|
||||
arrayField: [
|
||||
{ a: 'changed', b: 'changed', c: 'original' },
|
||||
{ a: 'changed', b: 'changed', c: 'changed' },
|
||||
],
|
||||
}
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(5)
|
||||
})
|
||||
|
||||
it('should count changed fields inside arrays nested inside of collapsibles', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'A collapsible field',
|
||||
fields: [
|
||||
{
|
||||
name: 'arrayField',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = { arrayField: [{ a: 'original', b: 'original', c: 'original' }] }
|
||||
const version = { arrayField: [{ a: 'changed', b: 'changed', c: 'original' }] }
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should count changed fields inside blocks fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'blockA',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = {
|
||||
blocks: [
|
||||
{ blockType: 'blockA', a: 'original', b: 'original', c: 'original' },
|
||||
{ blockType: 'blockA', a: 'original', b: 'original' },
|
||||
],
|
||||
}
|
||||
const version = {
|
||||
blocks: [
|
||||
{ blockType: 'blockA', a: 'changed', b: 'changed', c: 'original' },
|
||||
{ blockType: 'blockA', a: 'changed', b: 'changed', c: 'changed' },
|
||||
],
|
||||
}
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(5)
|
||||
})
|
||||
|
||||
it('should count changed fields between blocks with different slugs', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'blockA',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'blockB',
|
||||
fields: [
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
{ name: 'd', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = {
|
||||
blocks: [{ blockType: 'blockA', a: 'removed', b: 'original', c: 'original' }],
|
||||
}
|
||||
const version = {
|
||||
blocks: [{ blockType: 'blockB', b: 'original', c: 'changed', d: 'new' }],
|
||||
}
|
||||
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
|
||||
describe('localized fields', () => {
|
||||
const locales = ['en', 'de']
|
||||
it('should count simple localized fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{ name: 'a', type: 'text', localized: true },
|
||||
{ name: 'b', type: 'text', localized: true },
|
||||
]
|
||||
const comparison = {
|
||||
a: { en: 'original', de: 'original' },
|
||||
b: { en: 'original', de: 'original' },
|
||||
}
|
||||
const version = {
|
||||
a: { en: 'changed', de: 'original' },
|
||||
b: { en: 'original', de: 'original' },
|
||||
}
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
it('should count multiple locales of the same localized field', () => {
|
||||
const locales = ['en', 'de']
|
||||
const fields: ClientField[] = [
|
||||
{ name: 'a', type: 'text', localized: true },
|
||||
{ name: 'b', type: 'text', localized: true },
|
||||
]
|
||||
const comparison = {
|
||||
a: { en: 'original', de: 'original' },
|
||||
b: { en: 'original', de: 'original' },
|
||||
}
|
||||
const version = {
|
||||
a: { en: 'changed', de: 'changed' },
|
||||
b: { en: 'original', de: 'original' },
|
||||
}
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should count changed fields inside localized groups fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = {
|
||||
group: {
|
||||
en: { a: 'original', b: 'original', c: 'original' },
|
||||
de: { a: 'original', b: 'original', c: 'original' },
|
||||
},
|
||||
}
|
||||
const version = {
|
||||
group: {
|
||||
en: { a: 'changed', b: 'changed', c: 'original' },
|
||||
de: { a: 'original', b: 'changed', c: 'original' },
|
||||
},
|
||||
}
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
it('should count changed fields inside localized tabs', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab',
|
||||
localized: true,
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = {
|
||||
tab: {
|
||||
en: { a: 'original', b: 'original', c: 'original' },
|
||||
de: { a: 'original', b: 'original', c: 'original' },
|
||||
},
|
||||
}
|
||||
const version = {
|
||||
tab: {
|
||||
en: { a: 'changed', b: 'changed', c: 'original' },
|
||||
de: { a: 'original', b: 'changed', c: 'original' },
|
||||
},
|
||||
}
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
|
||||
it('should count changed fields inside localized array fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
name: 'arrayField',
|
||||
type: 'array',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'a',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = {
|
||||
arrayField: {
|
||||
en: [{ a: 'original', b: 'original', c: 'original' }],
|
||||
de: [{ a: 'original', b: 'original', c: 'original' }],
|
||||
},
|
||||
}
|
||||
const version = {
|
||||
arrayField: {
|
||||
en: [{ a: 'changed', b: 'changed', c: 'original' }],
|
||||
de: [{ a: 'original', b: 'changed', c: 'original' }],
|
||||
},
|
||||
}
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
|
||||
it('should count changed fields inside localized blocks fields', () => {
|
||||
const fields: ClientField[] = [
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
localized: true,
|
||||
blocks: [
|
||||
{
|
||||
slug: 'blockA',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const comparison = {
|
||||
blocks: {
|
||||
en: [{ blockType: 'blockA', a: 'original', b: 'original', c: 'original' }],
|
||||
de: [{ blockType: 'blockA', a: 'original', b: 'original', c: 'original' }],
|
||||
},
|
||||
}
|
||||
const version = {
|
||||
blocks: {
|
||||
en: [{ blockType: 'blockA', a: 'changed', b: 'changed', c: 'original' }],
|
||||
de: [{ blockType: 'blockA', a: 'original', b: 'changed', c: 'original' }],
|
||||
},
|
||||
}
|
||||
const result = countChangedFields({ comparison, fields, version, locales })
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countChangedFieldsInRows', () => {
|
||||
it('should count fields in array rows', () => {
|
||||
const field: ClientField = {
|
||||
name: 'myArray',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
}
|
||||
|
||||
const comparisonRows = [{ a: 'original', b: 'original', c: 'original' }]
|
||||
const versionRows = [{ a: 'changed', b: 'changed', c: 'original' }]
|
||||
|
||||
const result = countChangedFieldsInRows({
|
||||
comparisonRows,
|
||||
field,
|
||||
locales: undefined,
|
||||
versionRows,
|
||||
})
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it('should count fields in blocks', () => {
|
||||
const field: ClientField = {
|
||||
name: 'myBlocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'blockA',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const comparisonRows = [{ blockType: 'blockA', a: 'original', b: 'original', c: 'original' }]
|
||||
const versionRows = [{ blockType: 'blockA', a: 'changed', b: 'changed', c: 'original' }]
|
||||
|
||||
const result = countChangedFieldsInRows({
|
||||
comparisonRows,
|
||||
field,
|
||||
locales: undefined,
|
||||
versionRows,
|
||||
})
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,197 @@
|
||||
import type { ArrayFieldClient, BlocksFieldClient, ClientField } from 'payload'
|
||||
|
||||
import { fieldHasChanges } from './fieldHasChanges.js'
|
||||
import { getFieldsForRowComparison } from './getFieldsForRowComparison.js'
|
||||
|
||||
type Args = {
|
||||
comparison: unknown
|
||||
fields: ClientField[]
|
||||
locales: string[] | undefined
|
||||
version: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively counts the number of changed fields between comparison and
|
||||
* version data for a given set of fields.
|
||||
*/
|
||||
export function countChangedFields({ comparison, fields, locales, version }: Args) {
|
||||
let count = 0
|
||||
|
||||
fields.forEach((field) => {
|
||||
// Don't count the id field since it is not displayed in the UI
|
||||
if ('name' in field && field.name === 'id') {
|
||||
return
|
||||
}
|
||||
const fieldType = field.type
|
||||
switch (fieldType) {
|
||||
// Iterable fields are arrays and blocks fields. We iterate over each row and
|
||||
// count the number of changed fields in each.
|
||||
case 'array':
|
||||
case 'blocks': {
|
||||
if (locales && field.localized) {
|
||||
locales.forEach((locale) => {
|
||||
const comparisonRows = comparison?.[field.name]?.[locale] ?? []
|
||||
const versionRows = version?.[field.name]?.[locale] ?? []
|
||||
count += countChangedFieldsInRows({ comparisonRows, field, locales, versionRows })
|
||||
})
|
||||
} else {
|
||||
const comparisonRows = comparison?.[field.name] ?? []
|
||||
const versionRows = version?.[field.name] ?? []
|
||||
count += countChangedFieldsInRows({ comparisonRows, field, locales, versionRows })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Regular fields without nested fields.
|
||||
case 'checkbox':
|
||||
case 'code':
|
||||
case 'date':
|
||||
case 'email':
|
||||
case 'join':
|
||||
case 'json':
|
||||
case 'number':
|
||||
case 'point':
|
||||
case 'radio':
|
||||
case 'relationship':
|
||||
case 'richText':
|
||||
case 'select':
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
case 'upload': {
|
||||
// Fields that have a name and contain data. We can just check if the data has changed.
|
||||
if (locales && field.localized) {
|
||||
locales.forEach((locale) => {
|
||||
if (
|
||||
fieldHasChanges(version?.[field.name]?.[locale], comparison?.[field.name]?.[locale])
|
||||
) {
|
||||
count++
|
||||
}
|
||||
})
|
||||
} else if (fieldHasChanges(version?.[field.name], comparison?.[field.name])) {
|
||||
count++
|
||||
}
|
||||
break
|
||||
}
|
||||
// Fields that have nested fields, but don't nest their fields' data.
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
count += countChangedFields({
|
||||
comparison,
|
||||
fields: field.fields,
|
||||
locales,
|
||||
version,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Fields that have nested fields and nest their fields' data.
|
||||
case 'group': {
|
||||
if (locales && field.localized) {
|
||||
locales.forEach((locale) => {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[field.name]?.[locale],
|
||||
fields: field.fields,
|
||||
locales,
|
||||
version: version?.[field.name]?.[locale],
|
||||
})
|
||||
})
|
||||
} else {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[field.name],
|
||||
fields: field.fields,
|
||||
locales,
|
||||
version: version?.[field.name],
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Each tab in a tabs field has nested fields. The fields data may be
|
||||
// nested or not depending on the existence of a name property.
|
||||
case 'tabs': {
|
||||
field.tabs.forEach((tab) => {
|
||||
if ('name' in tab && locales && tab.localized) {
|
||||
// Named localized tab
|
||||
locales.forEach((locale) => {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[tab.name]?.[locale],
|
||||
fields: tab.fields,
|
||||
locales,
|
||||
version: version?.[tab.name]?.[locale],
|
||||
})
|
||||
})
|
||||
} else if ('name' in tab) {
|
||||
// Named tab
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[tab.name],
|
||||
fields: tab.fields,
|
||||
locales,
|
||||
version: version?.[tab.name],
|
||||
})
|
||||
} else {
|
||||
// Unnamed tab
|
||||
count += countChangedFields({
|
||||
comparison,
|
||||
fields: tab.fields,
|
||||
locales,
|
||||
version,
|
||||
})
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// UI fields don't have data and are not displayed in the version view
|
||||
// so we can ignore them.
|
||||
case 'ui': {
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
const _exhaustiveCheck: never = fieldType
|
||||
throw new Error(`Unexpected field.type in countChangedFields : ${String(fieldType)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
type countChangedFieldsInRowsArgs = {
|
||||
comparisonRows: unknown[]
|
||||
field: ArrayFieldClient | BlocksFieldClient
|
||||
locales: string[] | undefined
|
||||
versionRows: unknown[]
|
||||
}
|
||||
|
||||
export function countChangedFieldsInRows({
|
||||
comparisonRows = [],
|
||||
field,
|
||||
locales,
|
||||
versionRows = [],
|
||||
}: countChangedFieldsInRowsArgs) {
|
||||
let count = 0
|
||||
let i = 0
|
||||
|
||||
while (comparisonRows[i] || versionRows[i]) {
|
||||
const comparisonRow = comparisonRows?.[i] || {}
|
||||
const versionRow = versionRows?.[i] || {}
|
||||
|
||||
const rowFields = getFieldsForRowComparison({
|
||||
comparisonRow,
|
||||
field,
|
||||
versionRow,
|
||||
})
|
||||
|
||||
count += countChangedFields({
|
||||
comparison: comparisonRow,
|
||||
fields: rowFields,
|
||||
locales,
|
||||
version: versionRow,
|
||||
})
|
||||
|
||||
i++
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { fieldHasChanges } from './fieldHasChanges.js'
|
||||
|
||||
describe('hasChanges', () => {
|
||||
it('should return false for identical values', () => {
|
||||
const a = 'value'
|
||||
const b = 'value'
|
||||
expect(fieldHasChanges(a, b)).toBe(false)
|
||||
})
|
||||
it('should return true for different values', () => {
|
||||
const a = 1
|
||||
const b = 2
|
||||
expect(fieldHasChanges(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for identical objects', () => {
|
||||
const a = { key: 'value' }
|
||||
const b = { key: 'value' }
|
||||
expect(fieldHasChanges(a, b)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for different objects', () => {
|
||||
const a = { key: 'value' }
|
||||
const b = { key: 'differentValue' }
|
||||
expect(fieldHasChanges(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
const a = { key: 'value' }
|
||||
const b = undefined
|
||||
expect(fieldHasChanges(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle null values', () => {
|
||||
const a = { key: 'value' }
|
||||
const b = null
|
||||
expect(fieldHasChanges(a, b)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export function fieldHasChanges(a: unknown, b: unknown) {
|
||||
return JSON.stringify(a) !== JSON.stringify(b)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { getFieldsForRowComparison } from './getFieldsForRowComparison'
|
||||
import type { ArrayFieldClient, BlocksFieldClient, ClientField } from 'payload'
|
||||
|
||||
describe('getFieldsForRowComparison', () => {
|
||||
describe('array fields', () => {
|
||||
it('should return fields from array field', () => {
|
||||
const arrayFields: ClientField[] = [
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'description', type: 'textarea' },
|
||||
]
|
||||
|
||||
const field: ArrayFieldClient = {
|
||||
type: 'array',
|
||||
name: 'items',
|
||||
fields: arrayFields,
|
||||
}
|
||||
|
||||
const result = getFieldsForRowComparison({
|
||||
field,
|
||||
versionRow: {},
|
||||
comparisonRow: {},
|
||||
})
|
||||
|
||||
expect(result).toEqual(arrayFields)
|
||||
})
|
||||
})
|
||||
|
||||
describe('blocks fields', () => {
|
||||
it('should return combined fields when block types match', () => {
|
||||
const blockAFields: ClientField[] = [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
]
|
||||
|
||||
const field: BlocksFieldClient = {
|
||||
type: 'blocks',
|
||||
name: 'myBlocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'blockA',
|
||||
fields: blockAFields,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const versionRow = { blockType: 'blockA' }
|
||||
const comparisonRow = { blockType: 'blockA' }
|
||||
|
||||
const result = getFieldsForRowComparison({
|
||||
field,
|
||||
versionRow,
|
||||
comparisonRow,
|
||||
})
|
||||
|
||||
expect(result).toEqual(blockAFields)
|
||||
})
|
||||
|
||||
it('should return unique combined fields when block types differ', () => {
|
||||
const field: BlocksFieldClient = {
|
||||
type: 'blocks',
|
||||
name: 'myBlocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'blockA',
|
||||
fields: [
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'blockB',
|
||||
fields: [
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const versionRow = { blockType: 'blockA' }
|
||||
const comparisonRow = { blockType: 'blockB' }
|
||||
|
||||
const result = getFieldsForRowComparison({
|
||||
field,
|
||||
versionRow,
|
||||
comparisonRow,
|
||||
})
|
||||
|
||||
// Should contain all unique fields from both blocks
|
||||
expect(result).toEqual([
|
||||
{ name: 'a', type: 'text' },
|
||||
{ name: 'b', type: 'text' },
|
||||
{ name: 'c', type: 'text' },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { ArrayFieldClient, BlocksFieldClient, ClientField } from 'payload'
|
||||
|
||||
import { getUniqueListBy } from 'payload/shared'
|
||||
|
||||
/**
|
||||
* Get the fields for a row in an iterable field for comparison.
|
||||
* - Array fields: the fields of the array field, because the fields are the same for each row.
|
||||
* - Blocks fields: the union of fields from the comparison and version row,
|
||||
* because the fields from the version and comparison rows may differ.
|
||||
*/
|
||||
export function getFieldsForRowComparison({
|
||||
comparisonRow,
|
||||
field,
|
||||
versionRow,
|
||||
}: {
|
||||
comparisonRow: any
|
||||
field: ArrayFieldClient | BlocksFieldClient
|
||||
versionRow: any
|
||||
}) {
|
||||
let fields: ClientField[] = []
|
||||
|
||||
if (field.type === 'array' && 'fields' in field) {
|
||||
fields = field.fields
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
if (versionRow?.blockType === comparisonRow?.blockType) {
|
||||
const matchedBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
|
||||
fields = matchedBlock.fields
|
||||
} else {
|
||||
const matchedVersionBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
const matchedComparisonBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === comparisonRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
|
||||
fields = getUniqueListBy<ClientField>(
|
||||
[...matchedVersionBlock.fields, ...matchedComparisonBlock.fields],
|
||||
'name',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
@@ -352,6 +352,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
|
||||
'version:autosavedSuccessfully',
|
||||
'version:autosavedVersion',
|
||||
'version:changed',
|
||||
'version:changedFieldsCount',
|
||||
'version:confirmRevertToSaved',
|
||||
'version:compareVersion',
|
||||
'version:confirmPublish',
|
||||
|
||||
@@ -435,6 +435,8 @@ export const arTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'تمّ الحفظ التّلقائي بنجاح.',
|
||||
autosavedVersion: 'النّسخة المحفوظة تلقائياً',
|
||||
changed: 'تمّ التّغيير',
|
||||
changedFieldsCount_one: '{{count}} قام بتغيير الحقل',
|
||||
changedFieldsCount_other: '{{count}} حقول تم تغييرها',
|
||||
compareVersion: 'مقارنة النّسخة مع:',
|
||||
confirmPublish: 'تأكيد النّشر',
|
||||
confirmRevertToSaved: 'تأكيد الرّجوع للنسخة المنشورة',
|
||||
|
||||
@@ -444,6 +444,8 @@ export const azTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Uğurla avtomatik olaraq yadda saxlandı.',
|
||||
autosavedVersion: 'Avtomatik yadda saxlanmış versiya',
|
||||
changed: 'Dəyişdirildi',
|
||||
changedFieldsCount_one: '{{count}} sahə dəyişdi',
|
||||
changedFieldsCount_other: '{{count}} dəyişdirilmiş sahələr',
|
||||
compareVersion: 'Versiyanı müqayisə et:',
|
||||
confirmPublish: 'Dərci təsdiq edin',
|
||||
confirmRevertToSaved: 'Yadda saxlanana qayıtmağı təsdiq edin',
|
||||
|
||||
@@ -443,6 +443,8 @@ export const bgTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Успешно автоматично запазване.',
|
||||
autosavedVersion: 'Автоматично запазена версия',
|
||||
changed: 'Променен',
|
||||
changedFieldsCount_one: '{{count}} променено поле',
|
||||
changedFieldsCount_other: '{{count}} променени полета',
|
||||
compareVersion: 'Сравни версия с:',
|
||||
confirmPublish: 'Потвърди публикуване',
|
||||
confirmRevertToSaved: 'Потвърди възстановяване до запазен',
|
||||
|
||||
@@ -446,6 +446,8 @@ export const caTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Desat automàticament amb èxit.',
|
||||
autosavedVersion: 'Versió desada automàticament',
|
||||
changed: 'Canviat',
|
||||
changedFieldsCount_one: '{{count}} camp canviat',
|
||||
changedFieldsCount_other: '{{count}} camps modificats',
|
||||
compareVersion: 'Comparar versió amb:',
|
||||
confirmPublish: 'Confirmar publicació',
|
||||
confirmRevertToSaved: 'Confirmar revertir a desat',
|
||||
|
||||
@@ -440,6 +440,8 @@ export const csTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Úspěšně uloženo automaticky.',
|
||||
autosavedVersion: 'Verze automatického uložení',
|
||||
changed: 'Změněno',
|
||||
changedFieldsCount_one: '{{count}} změněné pole',
|
||||
changedFieldsCount_other: '{{count}} změněná pole',
|
||||
compareVersion: 'Porovnat verzi s:',
|
||||
confirmPublish: 'Potvrďte publikování',
|
||||
confirmRevertToSaved: 'Potvrdit vrácení k uloženému',
|
||||
|
||||
@@ -442,6 +442,8 @@ export const daTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Autosaved gennemført.',
|
||||
autosavedVersion: 'Autosaved version',
|
||||
changed: 'Ændret',
|
||||
changedFieldsCount_one: '{{count}} ændret felt',
|
||||
changedFieldsCount_other: '{{count}} ændrede felter',
|
||||
compareVersion: 'Sammenlign version med:',
|
||||
confirmPublish: 'Bekræft offentliggørelse',
|
||||
confirmRevertToSaved: 'Bekræft tilbagerulning til gemt',
|
||||
|
||||
@@ -448,6 +448,8 @@ export const deTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Erfolgreich automatisch gespeichert.',
|
||||
autosavedVersion: 'Automatisch gespeicherte Version',
|
||||
changed: 'Geändert',
|
||||
changedFieldsCount_one: '{{count}} geändertes Feld',
|
||||
changedFieldsCount_other: '{{count}} geänderte Felder',
|
||||
compareVersion: 'Vergleiche Version zu:',
|
||||
confirmPublish: 'Veröffentlichung bestätigen',
|
||||
confirmRevertToSaved: 'Zurücksetzen auf die letzte Speicherung bestätigen',
|
||||
|
||||
@@ -443,6 +443,8 @@ export const enTranslations = {
|
||||
autosave: 'Autosave',
|
||||
autosavedSuccessfully: 'Autosaved successfully.',
|
||||
autosavedVersion: 'Autosaved version',
|
||||
changedFieldsCount_one: '{{count}} changed field',
|
||||
changedFieldsCount_other: '{{count}} changed fields',
|
||||
changed: 'Changed',
|
||||
compareVersion: 'Compare version against:',
|
||||
confirmPublish: 'Confirm publish',
|
||||
|
||||
@@ -448,6 +448,8 @@ export const esTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Guardado automáticamente con éxito.',
|
||||
autosavedVersion: 'Versión Autoguardada',
|
||||
changed: 'Modificado',
|
||||
changedFieldsCount_one: '{{count}} campo modificado',
|
||||
changedFieldsCount_other: '{{count}} campos modificados',
|
||||
compareVersion: 'Comparar versión con:',
|
||||
confirmPublish: 'Confirmar publicación',
|
||||
confirmRevertToSaved: 'Confirmar revertir a guardado',
|
||||
|
||||
@@ -437,6 +437,8 @@ export const etTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Automaatselt salvestatud.',
|
||||
autosavedVersion: 'Automaatselt salvestatud versioon',
|
||||
changed: 'Muudetud',
|
||||
changedFieldsCount_one: '{{count}} muudetud väli',
|
||||
changedFieldsCount_other: '{{count}} muudetud välja',
|
||||
compareVersion: 'Võrdle versiooni:',
|
||||
confirmPublish: 'Kinnita avaldamine',
|
||||
confirmRevertToSaved: 'Kinnita taastamine salvestatud seisundisse',
|
||||
|
||||
@@ -439,6 +439,8 @@ export const faTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'با موفقیت ذخیره خودکار شد.',
|
||||
autosavedVersion: 'نگارش ذخیره شده خودکار',
|
||||
changed: 'تغییر کرد',
|
||||
changedFieldsCount_one: '{{count}} فیلد تغییر کرد',
|
||||
changedFieldsCount_other: '{{count}} فیلدهای تغییر یافته',
|
||||
compareVersion: 'مقایسه نگارش با:',
|
||||
confirmPublish: 'تأیید انتشار',
|
||||
confirmRevertToSaved: 'تأیید بازگردانی نگارش ذخیره شده',
|
||||
|
||||
@@ -456,6 +456,8 @@ export const frTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Enregistrement automatique réussi.',
|
||||
autosavedVersion: 'Version enregistrée automatiquement',
|
||||
changed: 'Modifié',
|
||||
changedFieldsCount_one: '{{count}} champ modifié',
|
||||
changedFieldsCount_other: '{{count}} champs modifiés',
|
||||
compareVersion: 'Comparez cette version à :',
|
||||
confirmPublish: 'Confirmer la publication',
|
||||
confirmRevertToSaved: 'Confirmer la restauration',
|
||||
|
||||
@@ -429,6 +429,8 @@ export const heTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'נשמר בהצלחה.',
|
||||
autosavedVersion: 'גרסת שמירה אוטומטית',
|
||||
changed: 'שונה',
|
||||
changedFieldsCount_one: '{{count}} שינה שדה',
|
||||
changedFieldsCount_other: '{{count}} שדות ששונו',
|
||||
compareVersion: 'השווה לגרסה:',
|
||||
confirmPublish: 'אישור פרסום',
|
||||
confirmRevertToSaved: 'אישור שחזור לגרסה שנשמרה',
|
||||
|
||||
@@ -440,6 +440,8 @@ export const hrTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Automatsko spremanje uspješno.',
|
||||
autosavedVersion: 'Verzija automatski spremljenog dokumenta',
|
||||
changed: 'Promijenjeno',
|
||||
changedFieldsCount_one: '{{count}} promijenjeno polje',
|
||||
changedFieldsCount_other: '{{count}} promijenjena polja',
|
||||
compareVersion: 'Usporedi verziju sa:',
|
||||
confirmPublish: 'Potvrdi objavu',
|
||||
confirmRevertToSaved: 'Potvrdite vraćanje na spremljeno',
|
||||
|
||||
@@ -448,6 +448,8 @@ export const huTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Automatikus mentés sikeres.',
|
||||
autosavedVersion: 'Automatikusan mentett verzió',
|
||||
changed: 'Megváltozott',
|
||||
changedFieldsCount_one: '{{count}} megváltozott mező',
|
||||
changedFieldsCount_other: '{{count}} módosított mező',
|
||||
compareVersion: 'Hasonlítsa össze a verziót a következőkkel:',
|
||||
confirmPublish: 'A közzététel megerősítése',
|
||||
confirmRevertToSaved: 'Erősítse meg a mentett verzióra való visszatérést',
|
||||
|
||||
@@ -448,6 +448,8 @@ export const itTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Salvataggio automatico riuscito.',
|
||||
autosavedVersion: 'Versione salvata automaticamente',
|
||||
changed: 'Modificato',
|
||||
changedFieldsCount_one: '{{count}} campo modificato',
|
||||
changedFieldsCount_other: '{{count}} campi modificati',
|
||||
compareVersion: 'Confronta versione con:',
|
||||
confirmPublish: 'Conferma la pubblicazione',
|
||||
confirmRevertToSaved: 'Conferma il ripristino dei salvataggi',
|
||||
|
||||
@@ -441,6 +441,8 @@ export const jaTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: '自動保存に成功しました。',
|
||||
autosavedVersion: '自動保存されたバージョン',
|
||||
changed: '変更済み',
|
||||
changedFieldsCount_one: '{{count}} 変更されたフィールド',
|
||||
changedFieldsCount_other: '{{count}}つの変更されたフィールド',
|
||||
compareVersion: 'バージョンを比較:',
|
||||
confirmPublish: '公開を確認する',
|
||||
confirmRevertToSaved: '保存された状態に戻す確認',
|
||||
|
||||
@@ -436,6 +436,8 @@ export const koTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: '자동 저장이 완료되었습니다.',
|
||||
autosavedVersion: '자동 저장된 버전',
|
||||
changed: '변경됨',
|
||||
changedFieldsCount_one: '{{count}} 변경된 필드',
|
||||
changedFieldsCount_other: '{{count}}개의 변경된 필드',
|
||||
compareVersion: '비교할 버전 선택:',
|
||||
confirmPublish: '게시하기',
|
||||
confirmRevertToSaved: '저장된 상태로 되돌리기',
|
||||
|
||||
@@ -451,6 +451,8 @@ export const myTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'အလိုအလျောက် သိမ်းဆည်းပြီးပါပြီ။',
|
||||
autosavedVersion: 'အော်တို ဗားရှင်း',
|
||||
changed: 'ပြောင်းခဲ့သည်။',
|
||||
changedFieldsCount_one: '{{count}} field telah diubah',
|
||||
changedFieldsCount_other: '{{count}}ကယ်လက်ရှိအရာများပြောင်းလဲလိုက်သည်',
|
||||
compareVersion: 'ဗားရှင်းနှင့် နှိုင်းယှဉ်ချက်:',
|
||||
confirmPublish: 'ထုတ်ဝေအတည်ပြုပါ။',
|
||||
confirmRevertToSaved: 'သိမ်းဆည်းပြီးကြောင်း အတည်ပြုပါ။',
|
||||
|
||||
@@ -444,6 +444,8 @@ export const nbTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Lagret automatisk.',
|
||||
autosavedVersion: 'Automatisk lagret versjon',
|
||||
changed: 'Endret',
|
||||
changedFieldsCount_one: '{{count}} endret felt',
|
||||
changedFieldsCount_other: '{{count}} endrede felt',
|
||||
compareVersion: 'Sammenlign versjon mot:',
|
||||
confirmPublish: 'Bekreft publisering',
|
||||
confirmRevertToSaved: 'Bekreft tilbakestilling til lagret',
|
||||
|
||||
@@ -448,6 +448,8 @@ export const nlTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Succesvol automatisch bewaard.',
|
||||
autosavedVersion: 'Automatisch bewaarde versie',
|
||||
changed: 'Gewijzigd',
|
||||
changedFieldsCount_one: '{{count}} gewijzigd veld',
|
||||
changedFieldsCount_other: '{{count}} gewijzigde velden',
|
||||
compareVersion: 'Vergelijk versie met:',
|
||||
confirmPublish: 'Bevestig publiceren',
|
||||
confirmRevertToSaved: 'Bevestig terugdraaien naar bewaarde versie',
|
||||
|
||||
@@ -443,6 +443,8 @@ export const plTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Pomyślnie zapisano automatycznie.',
|
||||
autosavedVersion: 'Wersja zapisana automatycznie',
|
||||
changed: 'Zmieniono',
|
||||
changedFieldsCount_one: '{{count}} zmienione pole',
|
||||
changedFieldsCount_other: '{{count}} zmienione pola',
|
||||
compareVersion: 'Porównaj wersję z:',
|
||||
confirmPublish: 'Potwierdź publikację',
|
||||
confirmRevertToSaved: 'Potwierdź powrót do zapisanego',
|
||||
|
||||
@@ -444,6 +444,8 @@ export const ptTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Salvamento automático com sucesso.',
|
||||
autosavedVersion: 'Versão de salvamento automático',
|
||||
changed: 'Alterado',
|
||||
changedFieldsCount_one: '{{count}} campo alterado',
|
||||
changedFieldsCount_other: '{{count}} campos alterados',
|
||||
compareVersion: 'Comparar versão com:',
|
||||
confirmPublish: 'Confirmar publicação',
|
||||
confirmRevertToSaved: 'Confirmar a reversão para o salvo',
|
||||
|
||||
@@ -451,6 +451,8 @@ export const roTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Autosalvare cu succes.',
|
||||
autosavedVersion: 'Versiunea salvată automat.',
|
||||
changed: 'Schimbat',
|
||||
changedFieldsCount_one: '{{count}} a modificat câmpul',
|
||||
changedFieldsCount_other: '{{count}} câmpuri modificate',
|
||||
compareVersion: 'Comparați versiunea cu:',
|
||||
confirmPublish: 'Confirmați publicarea',
|
||||
confirmRevertToSaved: 'Confirmați revenirea la starea salvată',
|
||||
|
||||
@@ -439,6 +439,8 @@ export const rsTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Аутоматско чување успешно.',
|
||||
autosavedVersion: 'Верзија аутоматски сачуваног документа',
|
||||
changed: 'Промењено',
|
||||
changedFieldsCount_one: '{{count}} promenjeno polje',
|
||||
changedFieldsCount_other: '{{count}} promenjena polja',
|
||||
compareVersion: 'Упореди верзију са:',
|
||||
confirmPublish: 'Потврди објаву',
|
||||
confirmRevertToSaved: 'Потврдите враћање на сачувано',
|
||||
|
||||
@@ -441,6 +441,8 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Automatsko čuvanje uspešno.',
|
||||
autosavedVersion: 'Verzija automatski sačuvanog dokumenta',
|
||||
changed: 'Promenjeno',
|
||||
changedFieldsCount_one: '{{count}} promenjeno polje',
|
||||
changedFieldsCount_other: '{{count}} promenjenih polja',
|
||||
compareVersion: 'Uporedi verziju sa:',
|
||||
confirmPublish: 'Potvrdi objavu',
|
||||
confirmRevertToSaved: 'Potvrdite vraćanje na sačuvano',
|
||||
|
||||
@@ -446,6 +446,8 @@ export const ruTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Автосохранение успешно.',
|
||||
autosavedVersion: 'Автоматически сохраненная версия',
|
||||
changed: 'Изменено',
|
||||
changedFieldsCount_one: '{{count}} изменил поле',
|
||||
changedFieldsCount_other: '{{count}} измененных полей',
|
||||
compareVersion: 'Сравнить версию с:',
|
||||
confirmPublish: 'Подтвердить публикацию',
|
||||
confirmRevertToSaved: 'Подтвердить возврат к сохраненному',
|
||||
|
||||
@@ -443,6 +443,8 @@ export const skTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Úspešne uložené automaticky.',
|
||||
autosavedVersion: 'Verzia automatického uloženia',
|
||||
changed: 'Zmenené',
|
||||
changedFieldsCount_one: '{{count}} zmenené pole',
|
||||
changedFieldsCount_other: '{{count}} zmenených polí',
|
||||
compareVersion: 'Porovnať verziu s:',
|
||||
confirmPublish: 'Potvrdiť publikovanie',
|
||||
confirmRevertToSaved: 'Potvrdiť vrátenie k uloženému',
|
||||
|
||||
@@ -440,6 +440,8 @@ export const slTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Samodejno shranjeno uspešno.',
|
||||
autosavedVersion: 'Samodejno shranjena različica',
|
||||
changed: 'Spremenjeno',
|
||||
changedFieldsCount_one: '{{count}} spremenjeno polje',
|
||||
changedFieldsCount_other: '{{count}} spremenjena polja',
|
||||
compareVersion: 'Primerjaj različico z:',
|
||||
confirmPublish: 'Potrdi objavo',
|
||||
confirmRevertToSaved: 'Potrdi vrnitev na shranjeno',
|
||||
|
||||
@@ -443,6 +443,8 @@ export const svTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Autosparades framgångsrikt.',
|
||||
autosavedVersion: 'Autosparad version',
|
||||
changed: 'Ändrad',
|
||||
changedFieldsCount_one: '{{count}} ändrat fält',
|
||||
changedFieldsCount_other: '{{count}} ändrade fält',
|
||||
compareVersion: 'Jämför version med:',
|
||||
confirmPublish: 'Bekräfta publicering',
|
||||
confirmRevertToSaved: 'Bekräfta återgång till sparad',
|
||||
|
||||
@@ -434,6 +434,8 @@ export const thTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'บันทึกอัตโนมัติสำเร็จ',
|
||||
autosavedVersion: 'เวอร์ชันบันทึกอัตโนมัติ',
|
||||
changed: 'มีการแก้ไข',
|
||||
changedFieldsCount_one: '{{count}} เปลี่ยนฟิลด์',
|
||||
changedFieldsCount_other: '{{count}} ฟิลด์ที่มีการเปลี่ยนแปลง',
|
||||
compareVersion: 'เปรียบเทียบเวอร์ชันกับ:',
|
||||
confirmPublish: 'ยืนยันการเผยแพร่',
|
||||
confirmRevertToSaved: 'ยืนยันย้อนการแก้ไข',
|
||||
|
||||
@@ -445,6 +445,8 @@ export const trTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Otomatik kaydetme başarılı',
|
||||
autosavedVersion: 'Otomatik kayıtlı sürüm',
|
||||
changed: 'Değişiklik yapıldı',
|
||||
changedFieldsCount_one: '{{count}} alanı değişti',
|
||||
changedFieldsCount_other: '{{count}} değişen alan',
|
||||
compareVersion: 'Sürümü şununla karşılaştır:',
|
||||
confirmPublish: 'Yayınlamayı onayla',
|
||||
confirmRevertToSaved: 'Confirm revert to saved',
|
||||
|
||||
@@ -441,6 +441,8 @@ export const ukTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Автозбереження успішно виконано.',
|
||||
autosavedVersion: 'Автозбереження',
|
||||
changed: 'Змінено',
|
||||
changedFieldsCount_one: '{{count}} змінене поле',
|
||||
changedFieldsCount_other: '{{count}} змінених полів',
|
||||
compareVersion: 'Порівняти версію з:',
|
||||
confirmPublish: 'Підтвердити публікацію',
|
||||
confirmRevertToSaved: 'Підтвердити повернення до збереженого стану',
|
||||
|
||||
@@ -438,6 +438,8 @@ export const viTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: 'Đã tự động lưu thành công.',
|
||||
autosavedVersion: 'Các phiên bản từ việc tự động lưu dữ liệu',
|
||||
changed: 'Đã thay đổi',
|
||||
changedFieldsCount_one: '{{count}} đã thay đổi trường',
|
||||
changedFieldsCount_other: '{{count}} trường đã thay đổi',
|
||||
compareVersion: 'So sánh phiên bản này với:',
|
||||
confirmPublish: 'Xác nhận xuất bản',
|
||||
confirmRevertToSaved: 'Xác nhận, quay về trạng thái đã lưu',
|
||||
|
||||
@@ -424,6 +424,8 @@ export const zhTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: '自动保存成功。',
|
||||
autosavedVersion: '自动保存的版本',
|
||||
changed: '已更改',
|
||||
changedFieldsCount_one: '{{count}}已更改的字段',
|
||||
changedFieldsCount_other: '{{count}}已更改的字段',
|
||||
compareVersion: '对比版本:',
|
||||
confirmPublish: '确认发布',
|
||||
confirmRevertToSaved: '确认恢复到保存状态',
|
||||
|
||||
@@ -424,6 +424,8 @@ export const zhTwTranslations: DefaultTranslationsObject = {
|
||||
autosavedSuccessfully: '自動儲存成功。',
|
||||
autosavedVersion: '自動儲存的版本',
|
||||
changed: '已更改',
|
||||
changedFieldsCount_one: '{{count}} 更改了字段',
|
||||
changedFieldsCount_other: '{{count}}個已更改的欄位',
|
||||
compareVersion: '對比版本:',
|
||||
confirmPublish: '確認發佈',
|
||||
confirmRevertToSaved: '確認回復到儲存狀態',
|
||||
|
||||
@@ -133,5 +133,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--size-small {
|
||||
padding: 0 base(0.2);
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export type PillProps = {
|
||||
onClick?: () => void
|
||||
pillStyle?: 'dark' | 'error' | 'light' | 'light-gray' | 'success' | 'warning' | 'white'
|
||||
rounded?: boolean
|
||||
size?: 'medium' | 'small'
|
||||
to?: string
|
||||
}
|
||||
|
||||
@@ -76,12 +77,14 @@ const StaticPill: React.FC<PillProps> = (props) => {
|
||||
onClick,
|
||||
pillStyle = 'light',
|
||||
rounded,
|
||||
size = 'medium',
|
||||
to,
|
||||
} = props
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
`${baseClass}--style-${pillStyle}`,
|
||||
`${baseClass}--size-${size}`,
|
||||
className && className,
|
||||
to && `${baseClass}--has-link`,
|
||||
(to || onClick) && `${baseClass}--has-action`,
|
||||
@@ -115,7 +118,7 @@ const StaticPill: React.FC<PillProps> = (props) => {
|
||||
type={Element === 'button' ? 'button' : undefined}
|
||||
>
|
||||
<span className={`${baseClass}__label`}>{children}</span>
|
||||
{icon && <span className={`${baseClass}__icon`}>{icon}</span>}
|
||||
{Boolean(icon) && <span className={`${baseClass}__icon`}>{icon}</span>}
|
||||
</Element>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -841,15 +841,72 @@ describe('Versions', () => {
|
||||
await page.waitForURL(versionURL)
|
||||
await expect(page.locator('.render-field-diffs').first()).toBeVisible()
|
||||
|
||||
const blocksDiffLabel = page.locator('.field-diff-label', {
|
||||
hasText: exactText('Blocks Field'),
|
||||
})
|
||||
|
||||
const blocksDiffLabel = page.getByText('Blocks Field', { exact: true })
|
||||
await expect(blocksDiffLabel).toBeVisible()
|
||||
const blocksDiff = blocksDiffLabel.locator('+ .iterable-diff__wrap > .render-field-diffs')
|
||||
|
||||
const blocksDiff = page.locator('.iterable-diff', { has: blocksDiffLabel })
|
||||
await expect(blocksDiff).toBeVisible()
|
||||
const blockTypeDiffLabel = blocksDiff.locator('.render-field-diffs__field').first()
|
||||
await expect(blockTypeDiffLabel).toBeVisible()
|
||||
|
||||
const blocksDiffRows = blocksDiff.locator('.iterable-diff__rows')
|
||||
await expect(blocksDiffRows).toBeVisible()
|
||||
|
||||
const firstBlocksDiffRow = blocksDiffRows.locator('.iterable-diff__row').first()
|
||||
await expect(firstBlocksDiffRow).toBeVisible()
|
||||
|
||||
const firstBlockDiffLabel = firstBlocksDiffRow.getByText('Block 01', { exact: true })
|
||||
await expect(firstBlockDiffLabel).toBeVisible()
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
const blocksDiffLabel = page.getByText('Blocks Field', { exact: true })
|
||||
await expect(blocksDiffLabel).toBeVisible()
|
||||
|
||||
// Expect iterable rows diff to be visible
|
||||
const blocksDiff = page.locator('.iterable-diff', { has: blocksDiffLabel })
|
||||
await expect(blocksDiff).toBeVisible()
|
||||
|
||||
// Expect iterable change count to be visible
|
||||
const iterableChangeCount = blocksDiff.locator('.diff-collapser__field-change-count').first()
|
||||
await expect(iterableChangeCount).toHaveText('2 changed fields')
|
||||
|
||||
// Expect iterable rows to be visible
|
||||
const blocksDiffRows = blocksDiff.locator('.iterable-diff__rows')
|
||||
await expect(blocksDiffRows).toBeVisible()
|
||||
|
||||
// Expect first iterable row to be visible
|
||||
const firstBlocksDiffRow = blocksDiffRows.locator('.iterable-diff__row').first()
|
||||
await expect(firstBlocksDiffRow).toBeVisible()
|
||||
|
||||
// Expect first row change count to be visible
|
||||
const firstBlocksDiffRowChangeCount = firstBlocksDiffRow
|
||||
.locator('.diff-collapser__field-change-count')
|
||||
.first()
|
||||
await expect(firstBlocksDiffRowChangeCount).toHaveText('2 changed fields')
|
||||
|
||||
// Expect collapser content to be visible
|
||||
const diffCollapserContent = blocksDiffRows.locator('.diff-collapser__content')
|
||||
await expect(diffCollapserContent).toBeVisible()
|
||||
|
||||
// Expect toggle button to be visible
|
||||
const toggleButton = firstBlocksDiffRow.locator('.diff-collapser__toggle-button').first()
|
||||
await expect(toggleButton).toBeVisible()
|
||||
|
||||
// Collapse content
|
||||
await toggleButton.click()
|
||||
|
||||
// Expect collapser content to be hidden
|
||||
await expect(diffCollapserContent).toBeHidden()
|
||||
|
||||
// Uncollapse content
|
||||
await toggleButton.click()
|
||||
|
||||
// Expect collapser content to be visible
|
||||
await expect(diffCollapserContent).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user