From 828b3b71c04526a4e6435b9f39b890da92764b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Louren=C3=A7o?= <208149+franciscolourenco@users.noreply.github.com> Date: Sat, 25 Jan 2025 02:32:55 +0800 Subject: [PATCH] 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 ![comparison](https://github.com/user-attachments/assets/754be708-be6d-43b4-bbe3-5d64ab6a0f76) ### With locales ![comparison with locales](https://github.com/user-attachments/assets/02fb47fb-fa38-4195-8376-67bfda7f282d) -------------- - [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 - [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~ --- .../next/src/views/Version/Default/index.tsx | 2 +- .../DiffCollapser/index.scss | 46 ++ .../DiffCollapser/index.tsx | 113 ++++ .../fields/Collapsible/index.tsx | 47 ++ .../fields/Group/index.scss | 14 + .../RenderFieldsToDiff/fields/Group/index.tsx | 55 ++ .../fields/Iterable/index.scss | 15 +- .../fields/Iterable/index.tsx | 154 +++-- .../fields/Nested/index.scss | 14 - .../fields/Nested/index.tsx | 52 -- .../fields/Relationship/index.tsx | 4 +- .../RenderFieldsToDiff/fields/Row/index.tsx | 38 ++ .../fields/Select/index.tsx | 4 +- .../RenderFieldsToDiff/fields/Tabs/index.scss | 20 + .../RenderFieldsToDiff/fields/Tabs/index.tsx | 133 +++-- .../RenderFieldsToDiff/fields/Text/index.tsx | 4 +- .../RenderFieldsToDiff/fields/index.tsx | 20 +- .../RenderFieldsToDiff/fields/types.ts | 1 - .../Version/RenderFieldsToDiff/index.tsx | 11 +- .../utilities/countChangedFields.spec.ts | 538 ++++++++++++++++++ .../utilities/countChangedFields.ts | 197 +++++++ .../utilities/fieldHasChanges.spec.ts | 38 ++ .../utilities/fieldHasChanges.ts | 3 + .../getFieldsForRowComparison.spec.ts | 97 ++++ .../utilities/getFieldsForRowComparison.ts | 52 ++ packages/translations/src/clientKeys.ts | 1 + packages/translations/src/languages/ar.ts | 2 + packages/translations/src/languages/az.ts | 2 + packages/translations/src/languages/bg.ts | 2 + packages/translations/src/languages/ca.ts | 2 + packages/translations/src/languages/cs.ts | 2 + packages/translations/src/languages/da.ts | 2 + packages/translations/src/languages/de.ts | 2 + packages/translations/src/languages/en.ts | 2 + packages/translations/src/languages/es.ts | 2 + packages/translations/src/languages/et.ts | 2 + packages/translations/src/languages/fa.ts | 2 + packages/translations/src/languages/fr.ts | 2 + packages/translations/src/languages/he.ts | 2 + packages/translations/src/languages/hr.ts | 2 + packages/translations/src/languages/hu.ts | 2 + packages/translations/src/languages/it.ts | 2 + packages/translations/src/languages/ja.ts | 2 + packages/translations/src/languages/ko.ts | 2 + packages/translations/src/languages/my.ts | 2 + packages/translations/src/languages/nb.ts | 2 + packages/translations/src/languages/nl.ts | 2 + packages/translations/src/languages/pl.ts | 2 + packages/translations/src/languages/pt.ts | 2 + packages/translations/src/languages/ro.ts | 2 + packages/translations/src/languages/rs.ts | 2 + .../translations/src/languages/rsLatin.ts | 2 + packages/translations/src/languages/ru.ts | 2 + packages/translations/src/languages/sk.ts | 2 + packages/translations/src/languages/sl.ts | 2 + packages/translations/src/languages/sv.ts | 2 + packages/translations/src/languages/th.ts | 2 + packages/translations/src/languages/tr.ts | 2 + packages/translations/src/languages/uk.ts | 2 + packages/translations/src/languages/vi.ts | 2 + packages/translations/src/languages/zh.ts | 2 + packages/translations/src/languages/zhTw.ts | 2 + packages/ui/src/elements/Pill/index.scss | 5 + packages/ui/src/elements/Pill/index.tsx | 5 +- test/versions/e2e.spec.ts | 71 ++- 65 files changed, 1585 insertions(+), 241 deletions(-) create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.scss delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.tsx create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Row/index.tsx create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.spec.ts create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.spec.ts create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.ts create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.spec.ts create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.ts diff --git a/packages/next/src/views/Version/Default/index.tsx b/packages/next/src/views/Version/Default/index.tsx index bcba3ee571..03c578f30b 100644 --- a/packages/next/src/views/Version/Default/index.tsx +++ b/packages/next/src/views/Version/Default/index.tsx @@ -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' diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss new file mode 100644 index 0000000000..6e5a902762 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss @@ -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; + } + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx new file mode 100644 index 0000000000..2c5a158e2a --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx @@ -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 = ({ + 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 ( +
+ +
{children}
+
+ ) +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx new file mode 100644 index 0000000000..9bc913cd04 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx @@ -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 = ({ + comparison, + diffComponents, + field, + fieldPermissions, + fields, + i18n, + locales, + version, +}) => { + return ( +
+ {getTranslation(field.label, i18n)} + } + locales={locales} + version={version} + > + + +
+ ) +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss new file mode 100644 index 0000000000..f44b931295 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss @@ -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); + } + } + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx new file mode 100644 index 0000000000..2d37095ff0 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx @@ -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 = ({ + comparison, + diffComponents, + field, + fieldPermissions, + fields, + i18n, + locale, + locales, + version, +}) => { + return ( +
+ + {locale && {locale}} + {getTranslation(field.label, i18n)} + + ) + } + locales={locales} + version={version} + > + + +
+ ) +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss index a795f2a757..577e849fbb 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss @@ -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 { diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx index de574b86b1..1ecf31ab96 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx @@ -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 = ({ +export const Iterable: React.FC = ({ comparison, diffComponents, field, @@ -27,88 +28,79 @@ const Iterable: React.FC = ({ 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 (
- {'label' in field && field.label && typeof field.label !== 'function' && ( - - )} - {maxRows > 0 && ( - - {Array.from(Array(maxRows).keys()).map((row, i) => { - const versionRow = version?.[i] || {} - const comparisonRow = comparison?.[i] || {} + + {locale && {locale}} + {getTranslation(field.label, i18n)} + + ) + } + locales={locales} + version={version} + > + {maxRows > 0 && ( +
+ {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( - [...fields, ...matchedVersionBlock.fields, ...matchedComparisonBlock.fields], - 'name', - ) - } - } - - return ( -
- -
- ) - })} - - )} - {maxRows === 0 && ( -
- {i18n.t('version:noRowsFound', { - label: - 'labels' in field && field.labels?.plural - ? getTranslation(field.labels.plural, i18n) - : i18n.t('general:rows'), - })} -
- )} + return ( +
+ + + +
+ ) + })} +
+ )} + {maxRows === 0 && ( +
+ {i18n.t('version:noRowsFound', { + label: + 'labels' in field && field.labels?.plural + ? getTranslation(field.labels.plural, i18n) + : i18n.t('general:rows'), + })} +
+ )} +
) } - -export default Iterable diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.scss deleted file mode 100644 index 5e6af60557..0000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.scss +++ /dev/null @@ -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); - } - } - } -} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.tsx deleted file mode 100644 index 6605d3ac44..0000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Nested/index.tsx +++ /dev/null @@ -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 = ({ - comparison, - diffComponents, - disableGutter = false, - field, - fieldPermissions, - fields, - i18n, - locale, - locales, - version, -}) => { - return ( -
- {'label' in field && field.label && typeof field.label !== 'function' && ( - - )} -
- -
-
- ) -} - -export default Nested diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx index df4f50b96f..dac1cd865a 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx @@ -99,7 +99,7 @@ const generateLabelFromValue = ( return valueToReturn } -const Relationship: React.FC> = ({ +export const Relationship: React.FC> = ({ comparison, field, i18n, @@ -159,5 +159,3 @@ const Relationship: React.FC> = ({ ) } - -export default Relationship diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Row/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Row/index.tsx new file mode 100644 index 0000000000..19bba3bbf9 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Row/index.tsx @@ -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 = ({ + comparison, + diffComponents, + field, + fieldPermissions, + fields, + i18n, + locales, + version, +}) => { + return ( +
+ {'label' in field && field.label && typeof field.label !== 'function' && ( + + )} + +
+ ) +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx index 46a474dde3..13d4be1a32 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx @@ -45,7 +45,7 @@ const getTranslatedOptions = ( return typeof options === 'string' ? options : getTranslation(options.label, i18n) } -const Select: React.FC> = ({ +export const Select: React.FC> = ({ comparison, diffMethod, field, @@ -87,5 +87,3 @@ const Select: React.FC> = ({ ) } - -export default Select diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss new file mode 100644 index 0000000000..715d8d81a9 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss @@ -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); + } + } + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx index ab1354c130..ad2da5e8e6 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx @@ -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> = ({ - comparison, - diffComponents, - field, - fieldPermissions, - i18n, - locale, - locales, - version, -}) => { +export const Tabs: React.FC> = (props) => { + const { comparison, field, locales, version } = props return (
-
- {field.tabs.map((tab, i) => { - if ('name' in tab) { - return ( - - ) - } - - return ( - - ) - })} -
+ {field.tabs.map((tab, i) => { + return ( +
+ {(() => { + 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 ( +
+
+ +
+
+ ) + }) + } else if ('name' in tab && tab.name) { + // Named tab + const namedTabProps = { + ...props, + comparison: comparison?.[tab.name], + version: version?.[tab.name], + } + return + } else { + // Unnamed tab + return + } + })()} +
+ ) + })}
) } -export default Tabs +type TabProps = { + tab: ClientTab +} & DiffComponentProps + +const Tab: React.FC = ({ + comparison, + diffComponents, + fieldPermissions, + i18n, + locale, + locales, + tab, + version, +}) => { + return ( + + {locale && {locale}} + {getTranslation(tab.label, i18n)} + + ) + } + locales={locales} + version={version} + > + + + ) +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx index 8b9520d5d4..3425eeb493 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx @@ -13,7 +13,7 @@ import './index.scss' const baseClass = 'text-diff' -const Text: React.FC> = ({ +export const Text: React.FC> = ({ comparison, diffMethod, field, @@ -58,5 +58,3 @@ const Text: React.FC> = ({ ) } - -export default Text diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.tsx index 5abd7426a5..1601fed504 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.tsx @@ -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, diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts b/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts index fc6f4961ff..c9d0097be2 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts @@ -9,7 +9,6 @@ export type DiffComponentProps = { readonly comparison: any readonly diffComponents: DiffComponents readonly diffMethod?: DiffMethod - readonly disableGutter?: boolean readonly field: TField readonly fieldPermissions?: | { diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx index 6b89ff0acd..b92772594c 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx @@ -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 = ({ +export const RenderFieldsToDiff: React.FC = ({ comparison, diffComponents: __diffComponents, fieldPermissions, @@ -128,13 +127,13 @@ const RenderFieldsToDiff: React.FC = ({ ) } - // 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 ( - = ({ ) } - -export default RenderFieldsToDiff diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.spec.ts b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.spec.ts new file mode 100644 index 0000000000..0de90ea93a --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.spec.ts @@ -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) + }) +}) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts new file mode 100644 index 0000000000..c9902e9122 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts @@ -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 +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.spec.ts b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.spec.ts new file mode 100644 index 0000000000..153fe1c5d7 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.spec.ts @@ -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) + }) +}) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.ts b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.ts new file mode 100644 index 0000000000..fa2a0659b7 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/fieldHasChanges.ts @@ -0,0 +1,3 @@ +export function fieldHasChanges(a: unknown, b: unknown) { + return JSON.stringify(a) !== JSON.stringify(b) +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.spec.ts b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.spec.ts new file mode 100644 index 0000000000..c9e9426284 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.spec.ts @@ -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' }, + ]) + }) + }) +}) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.ts b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.ts new file mode 100644 index 0000000000..4750f3f04f --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/getFieldsForRowComparison.ts @@ -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( + [...matchedVersionBlock.fields, ...matchedComparisonBlock.fields], + 'name', + ) + } + } + + return fields +} diff --git a/packages/translations/src/clientKeys.ts b/packages/translations/src/clientKeys.ts index 53468d0202..0ea8a47762 100644 --- a/packages/translations/src/clientKeys.ts +++ b/packages/translations/src/clientKeys.ts @@ -352,6 +352,7 @@ export const clientTranslationKeys = createClientTranslationKeys([ 'version:autosavedSuccessfully', 'version:autosavedVersion', 'version:changed', + 'version:changedFieldsCount', 'version:confirmRevertToSaved', 'version:compareVersion', 'version:confirmPublish', diff --git a/packages/translations/src/languages/ar.ts b/packages/translations/src/languages/ar.ts index 2295e13d41..df0a2a5226 100644 --- a/packages/translations/src/languages/ar.ts +++ b/packages/translations/src/languages/ar.ts @@ -435,6 +435,8 @@ export const arTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'تمّ الحفظ التّلقائي بنجاح.', autosavedVersion: 'النّسخة المحفوظة تلقائياً', changed: 'تمّ التّغيير', + changedFieldsCount_one: '{{count}} قام بتغيير الحقل', + changedFieldsCount_other: '{{count}} حقول تم تغييرها', compareVersion: 'مقارنة النّسخة مع:', confirmPublish: 'تأكيد النّشر', confirmRevertToSaved: 'تأكيد الرّجوع للنسخة المنشورة', diff --git a/packages/translations/src/languages/az.ts b/packages/translations/src/languages/az.ts index e5b32df868..7a5059ce43 100644 --- a/packages/translations/src/languages/az.ts +++ b/packages/translations/src/languages/az.ts @@ -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', diff --git a/packages/translations/src/languages/bg.ts b/packages/translations/src/languages/bg.ts index e74eb1a0dc..19c5901def 100644 --- a/packages/translations/src/languages/bg.ts +++ b/packages/translations/src/languages/bg.ts @@ -443,6 +443,8 @@ export const bgTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'Успешно автоматично запазване.', autosavedVersion: 'Автоматично запазена версия', changed: 'Променен', + changedFieldsCount_one: '{{count}} променено поле', + changedFieldsCount_other: '{{count}} променени полета', compareVersion: 'Сравни версия с:', confirmPublish: 'Потвърди публикуване', confirmRevertToSaved: 'Потвърди възстановяване до запазен', diff --git a/packages/translations/src/languages/ca.ts b/packages/translations/src/languages/ca.ts index 893b4a1aed..e8ede0ff11 100644 --- a/packages/translations/src/languages/ca.ts +++ b/packages/translations/src/languages/ca.ts @@ -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', diff --git a/packages/translations/src/languages/cs.ts b/packages/translations/src/languages/cs.ts index e51ef733b2..8962ebca03 100644 --- a/packages/translations/src/languages/cs.ts +++ b/packages/translations/src/languages/cs.ts @@ -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', diff --git a/packages/translations/src/languages/da.ts b/packages/translations/src/languages/da.ts index e9ce345b7c..30ef653b09 100644 --- a/packages/translations/src/languages/da.ts +++ b/packages/translations/src/languages/da.ts @@ -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', diff --git a/packages/translations/src/languages/de.ts b/packages/translations/src/languages/de.ts index 89cfe2e5d5..6cb2d06e14 100644 --- a/packages/translations/src/languages/de.ts +++ b/packages/translations/src/languages/de.ts @@ -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', diff --git a/packages/translations/src/languages/en.ts b/packages/translations/src/languages/en.ts index 0e3bc5b531..1bbc227ec6 100644 --- a/packages/translations/src/languages/en.ts +++ b/packages/translations/src/languages/en.ts @@ -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', diff --git a/packages/translations/src/languages/es.ts b/packages/translations/src/languages/es.ts index c8c655830d..7b10fb255d 100644 --- a/packages/translations/src/languages/es.ts +++ b/packages/translations/src/languages/es.ts @@ -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', diff --git a/packages/translations/src/languages/et.ts b/packages/translations/src/languages/et.ts index 0325ea9224..6f0507cce1 100644 --- a/packages/translations/src/languages/et.ts +++ b/packages/translations/src/languages/et.ts @@ -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', diff --git a/packages/translations/src/languages/fa.ts b/packages/translations/src/languages/fa.ts index b3d18cfdbd..95529274db 100644 --- a/packages/translations/src/languages/fa.ts +++ b/packages/translations/src/languages/fa.ts @@ -439,6 +439,8 @@ export const faTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'با موفقیت ذخیره خودکار شد.', autosavedVersion: 'نگارش ذخیره شده خودکار', changed: 'تغییر کرد', + changedFieldsCount_one: '{{count}} فیلد تغییر کرد', + changedFieldsCount_other: '{{count}} فیلدهای تغییر یافته', compareVersion: 'مقایسه نگارش با:', confirmPublish: 'تأیید انتشار', confirmRevertToSaved: 'تأیید بازگردانی نگارش ذخیره شده', diff --git a/packages/translations/src/languages/fr.ts b/packages/translations/src/languages/fr.ts index 8b6ad1254b..7c63a823e9 100644 --- a/packages/translations/src/languages/fr.ts +++ b/packages/translations/src/languages/fr.ts @@ -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', diff --git a/packages/translations/src/languages/he.ts b/packages/translations/src/languages/he.ts index 86f5f8ca36..0555971150 100644 --- a/packages/translations/src/languages/he.ts +++ b/packages/translations/src/languages/he.ts @@ -429,6 +429,8 @@ export const heTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'נשמר בהצלחה.', autosavedVersion: 'גרסת שמירה אוטומטית', changed: 'שונה', + changedFieldsCount_one: '{{count}} שינה שדה', + changedFieldsCount_other: '{{count}} שדות ששונו', compareVersion: 'השווה לגרסה:', confirmPublish: 'אישור פרסום', confirmRevertToSaved: 'אישור שחזור לגרסה שנשמרה', diff --git a/packages/translations/src/languages/hr.ts b/packages/translations/src/languages/hr.ts index 5294f80113..91dbd03b2d 100644 --- a/packages/translations/src/languages/hr.ts +++ b/packages/translations/src/languages/hr.ts @@ -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', diff --git a/packages/translations/src/languages/hu.ts b/packages/translations/src/languages/hu.ts index ab1e8b3c53..1a4dc3d181 100644 --- a/packages/translations/src/languages/hu.ts +++ b/packages/translations/src/languages/hu.ts @@ -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', diff --git a/packages/translations/src/languages/it.ts b/packages/translations/src/languages/it.ts index 2a2af93b07..8c68f455e6 100644 --- a/packages/translations/src/languages/it.ts +++ b/packages/translations/src/languages/it.ts @@ -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', diff --git a/packages/translations/src/languages/ja.ts b/packages/translations/src/languages/ja.ts index ae98ded03e..184f1d32da 100644 --- a/packages/translations/src/languages/ja.ts +++ b/packages/translations/src/languages/ja.ts @@ -441,6 +441,8 @@ export const jaTranslations: DefaultTranslationsObject = { autosavedSuccessfully: '自動保存に成功しました。', autosavedVersion: '自動保存されたバージョン', changed: '変更済み', + changedFieldsCount_one: '{{count}} 変更されたフィールド', + changedFieldsCount_other: '{{count}}つの変更されたフィールド', compareVersion: 'バージョンを比較:', confirmPublish: '公開を確認する', confirmRevertToSaved: '保存された状態に戻す確認', diff --git a/packages/translations/src/languages/ko.ts b/packages/translations/src/languages/ko.ts index c731d34f04..d95769ea75 100644 --- a/packages/translations/src/languages/ko.ts +++ b/packages/translations/src/languages/ko.ts @@ -436,6 +436,8 @@ export const koTranslations: DefaultTranslationsObject = { autosavedSuccessfully: '자동 저장이 완료되었습니다.', autosavedVersion: '자동 저장된 버전', changed: '변경됨', + changedFieldsCount_one: '{{count}} 변경된 필드', + changedFieldsCount_other: '{{count}}개의 변경된 필드', compareVersion: '비교할 버전 선택:', confirmPublish: '게시하기', confirmRevertToSaved: '저장된 상태로 되돌리기', diff --git a/packages/translations/src/languages/my.ts b/packages/translations/src/languages/my.ts index 5bc8a73eb9..4d84397780 100644 --- a/packages/translations/src/languages/my.ts +++ b/packages/translations/src/languages/my.ts @@ -451,6 +451,8 @@ export const myTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'အလိုအလျောက် သိမ်းဆည်းပြီးပါပြီ။', autosavedVersion: 'အော်တို ဗားရှင်း', changed: 'ပြောင်းခဲ့သည်။', + changedFieldsCount_one: '{{count}} field telah diubah', + changedFieldsCount_other: '{{count}}ကယ်လက်ရှိအရာများပြောင်းလဲလိုက်သည်', compareVersion: 'ဗားရှင်းနှင့် နှိုင်းယှဉ်ချက်:', confirmPublish: 'ထုတ်ဝေအတည်ပြုပါ။', confirmRevertToSaved: 'သိမ်းဆည်းပြီးကြောင်း အတည်ပြုပါ။', diff --git a/packages/translations/src/languages/nb.ts b/packages/translations/src/languages/nb.ts index 524ffc119f..057fa8774e 100644 --- a/packages/translations/src/languages/nb.ts +++ b/packages/translations/src/languages/nb.ts @@ -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', diff --git a/packages/translations/src/languages/nl.ts b/packages/translations/src/languages/nl.ts index 2a7f311c6c..6955917b53 100644 --- a/packages/translations/src/languages/nl.ts +++ b/packages/translations/src/languages/nl.ts @@ -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', diff --git a/packages/translations/src/languages/pl.ts b/packages/translations/src/languages/pl.ts index 44bf33fc4b..5f082f5811 100644 --- a/packages/translations/src/languages/pl.ts +++ b/packages/translations/src/languages/pl.ts @@ -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', diff --git a/packages/translations/src/languages/pt.ts b/packages/translations/src/languages/pt.ts index 98ff9c2582..2b3bec1040 100644 --- a/packages/translations/src/languages/pt.ts +++ b/packages/translations/src/languages/pt.ts @@ -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', diff --git a/packages/translations/src/languages/ro.ts b/packages/translations/src/languages/ro.ts index a5c2be635e..c0ad5546cc 100644 --- a/packages/translations/src/languages/ro.ts +++ b/packages/translations/src/languages/ro.ts @@ -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ă', diff --git a/packages/translations/src/languages/rs.ts b/packages/translations/src/languages/rs.ts index c8de60359b..3bdb894b37 100644 --- a/packages/translations/src/languages/rs.ts +++ b/packages/translations/src/languages/rs.ts @@ -439,6 +439,8 @@ export const rsTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'Аутоматско чување успешно.', autosavedVersion: 'Верзија аутоматски сачуваног документа', changed: 'Промењено', + changedFieldsCount_one: '{{count}} promenjeno polje', + changedFieldsCount_other: '{{count}} promenjena polja', compareVersion: 'Упореди верзију са:', confirmPublish: 'Потврди објаву', confirmRevertToSaved: 'Потврдите враћање на сачувано', diff --git a/packages/translations/src/languages/rsLatin.ts b/packages/translations/src/languages/rsLatin.ts index d51ed91a20..b2eb2aacb1 100644 --- a/packages/translations/src/languages/rsLatin.ts +++ b/packages/translations/src/languages/rsLatin.ts @@ -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', diff --git a/packages/translations/src/languages/ru.ts b/packages/translations/src/languages/ru.ts index ea9043dcff..b7b8a1f2a4 100644 --- a/packages/translations/src/languages/ru.ts +++ b/packages/translations/src/languages/ru.ts @@ -446,6 +446,8 @@ export const ruTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'Автосохранение успешно.', autosavedVersion: 'Автоматически сохраненная версия', changed: 'Изменено', + changedFieldsCount_one: '{{count}} изменил поле', + changedFieldsCount_other: '{{count}} измененных полей', compareVersion: 'Сравнить версию с:', confirmPublish: 'Подтвердить публикацию', confirmRevertToSaved: 'Подтвердить возврат к сохраненному', diff --git a/packages/translations/src/languages/sk.ts b/packages/translations/src/languages/sk.ts index d675cb338a..549b91eb41 100644 --- a/packages/translations/src/languages/sk.ts +++ b/packages/translations/src/languages/sk.ts @@ -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', diff --git a/packages/translations/src/languages/sl.ts b/packages/translations/src/languages/sl.ts index a6cad9873a..217b722652 100644 --- a/packages/translations/src/languages/sl.ts +++ b/packages/translations/src/languages/sl.ts @@ -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', diff --git a/packages/translations/src/languages/sv.ts b/packages/translations/src/languages/sv.ts index 9a7131cee8..6871945038 100644 --- a/packages/translations/src/languages/sv.ts +++ b/packages/translations/src/languages/sv.ts @@ -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', diff --git a/packages/translations/src/languages/th.ts b/packages/translations/src/languages/th.ts index 4b18cc5380..d32ffbcdf6 100644 --- a/packages/translations/src/languages/th.ts +++ b/packages/translations/src/languages/th.ts @@ -434,6 +434,8 @@ export const thTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'บันทึกอัตโนมัติสำเร็จ', autosavedVersion: 'เวอร์ชันบันทึกอัตโนมัติ', changed: 'มีการแก้ไข', + changedFieldsCount_one: '{{count}} เปลี่ยนฟิลด์', + changedFieldsCount_other: '{{count}} ฟิลด์ที่มีการเปลี่ยนแปลง', compareVersion: 'เปรียบเทียบเวอร์ชันกับ:', confirmPublish: 'ยืนยันการเผยแพร่', confirmRevertToSaved: 'ยืนยันย้อนการแก้ไข', diff --git a/packages/translations/src/languages/tr.ts b/packages/translations/src/languages/tr.ts index bf9238d968..46829eebf8 100644 --- a/packages/translations/src/languages/tr.ts +++ b/packages/translations/src/languages/tr.ts @@ -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', diff --git a/packages/translations/src/languages/uk.ts b/packages/translations/src/languages/uk.ts index 5f08985036..fe389e1033 100644 --- a/packages/translations/src/languages/uk.ts +++ b/packages/translations/src/languages/uk.ts @@ -441,6 +441,8 @@ export const ukTranslations: DefaultTranslationsObject = { autosavedSuccessfully: 'Автозбереження успішно виконано.', autosavedVersion: 'Автозбереження', changed: 'Змінено', + changedFieldsCount_one: '{{count}} змінене поле', + changedFieldsCount_other: '{{count}} змінених полів', compareVersion: 'Порівняти версію з:', confirmPublish: 'Підтвердити публікацію', confirmRevertToSaved: 'Підтвердити повернення до збереженого стану', diff --git a/packages/translations/src/languages/vi.ts b/packages/translations/src/languages/vi.ts index 5895ade91a..b199887747 100644 --- a/packages/translations/src/languages/vi.ts +++ b/packages/translations/src/languages/vi.ts @@ -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', diff --git a/packages/translations/src/languages/zh.ts b/packages/translations/src/languages/zh.ts index ef23e7d72c..fe54e98695 100644 --- a/packages/translations/src/languages/zh.ts +++ b/packages/translations/src/languages/zh.ts @@ -424,6 +424,8 @@ export const zhTranslations: DefaultTranslationsObject = { autosavedSuccessfully: '自动保存成功。', autosavedVersion: '自动保存的版本', changed: '已更改', + changedFieldsCount_one: '{{count}}已更改的字段', + changedFieldsCount_other: '{{count}}已更改的字段', compareVersion: '对比版本:', confirmPublish: '确认发布', confirmRevertToSaved: '确认恢复到保存状态', diff --git a/packages/translations/src/languages/zhTw.ts b/packages/translations/src/languages/zhTw.ts index e185b45816..37e053203b 100644 --- a/packages/translations/src/languages/zhTw.ts +++ b/packages/translations/src/languages/zhTw.ts @@ -424,6 +424,8 @@ export const zhTwTranslations: DefaultTranslationsObject = { autosavedSuccessfully: '自動儲存成功。', autosavedVersion: '自動儲存的版本', changed: '已更改', + changedFieldsCount_one: '{{count}} 更改了字段', + changedFieldsCount_other: '{{count}}個已更改的欄位', compareVersion: '對比版本:', confirmPublish: '確認發佈', confirmRevertToSaved: '確認回復到儲存狀態', diff --git a/packages/ui/src/elements/Pill/index.scss b/packages/ui/src/elements/Pill/index.scss index 2394525fd8..3b946f806b 100644 --- a/packages/ui/src/elements/Pill/index.scss +++ b/packages/ui/src/elements/Pill/index.scss @@ -133,5 +133,10 @@ } } } + + &--size-small { + padding: 0 base(0.2); + line-height: 18px; + } } } diff --git a/packages/ui/src/elements/Pill/index.tsx b/packages/ui/src/elements/Pill/index.tsx index 42863f6a14..233e5cb12d 100644 --- a/packages/ui/src/elements/Pill/index.tsx +++ b/packages/ui/src/elements/Pill/index.tsx @@ -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 = (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 = (props) => { type={Element === 'button' ? 'button' : undefined} > {children} - {icon && {icon}} + {Boolean(icon) && {icon}} ) } diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index bc055e487a..04c7eb28b0 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -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() }) }) })