feat(next): version view overhaul (#12027)

#11769 improved the lexical version view diff component. This PR
improves the rest of the version view.

## What changed

- Column layout when selecting a version:
	- Previously: Selected version on the left, latest version on the left
- Now: Previous version on the left, previous version on the right
(mimics behavior of GitHub)
- Locale selector now displayed in pill selector, rather than
react-select
- Smoother, more reliable locale, modifiedOnly and version selection.
Now uses clean event callbacks rather than useEffects
- React-diff-viewer-continued has been replaced with the html differ we
use in lexical
- Updated Design for all field diffs
- Version columns now have a clearly defined separator line
- Fixed collapsibles showing in version view despite having no modified
fields if modifiedOnly is true
- New, redesigned header
	

## Screenshots

### Before

![CleanShot 2025-04-11 at 20 10
03@2x](https://github.com/user-attachments/assets/a93a500a-3cdd-4cf0-84dd-cf5481aac2b3)

![CleanShot 2025-04-11 at 20 10
28@2x](https://github.com/user-attachments/assets/59bc5885-cbaf-49ea-8d1d-8d145463fd80)

### After

![Screenshot 2025-06-09 at 17 43
49@2x](https://github.com/user-attachments/assets/f6ff0369-76c9-4c1c-9aa7-cbd88806ddc1)

![Screenshot 2025-06-09 at 17 44
50@2x](https://github.com/user-attachments/assets/db93a3db-48d6-4e5d-b080-86a34fff5d22)

![Screenshot 2025-06-09 at 17 45
19@2x](https://github.com/user-attachments/assets/27b6c720-05fe-4957-85af-1305d6b65cfd)

![Screenshot 2025-06-09 at 17 45
34@2x](https://github.com/user-attachments/assets/6d42f458-515a-4611-b27a-f4d6bafbf555)
This commit is contained in:
Alessio Gravili
2025-06-16 04:58:03 -07:00
committed by GitHub
parent 9943b3508d
commit 4e2e4d2aed
182 changed files with 4795 additions and 2211 deletions

View File

@@ -64,7 +64,7 @@
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
"clean:build:allowtgz": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/meta_*.json'",
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
"dev": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts",
"dev": "cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=16384\" tsx ./test/dev.ts",
"dev:generate-db-schema": "pnpm runts ./test/generateDatabaseSchema.ts",
"dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
@@ -111,7 +111,7 @@
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:types": "tstyche",
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
"translateNewKeys": "pnpm --filter translations run translateNewKeys"
"translateNewKeys": "pnpm --filter @tools/scripts run generateTranslations:core"
},
"lint-staged": {
"**/package.json": "sort-package-json",

View File

@@ -103,7 +103,6 @@
"http-status": "2.1.0",
"path-to-regexp": "6.3.0",
"qs-esm": "7.0.2",
"react-diff-viewer-continued": "4.0.5",
"sass": "1.77.4",
"uuid": "10.0.0"
},

View File

@@ -11,6 +11,17 @@ import { renderDocumentSlotsHandler } from '../views/Document/renderDocumentSlot
import { renderListHandler } from '../views/List/handleServerFunction.js'
import { initReq } from './initReq.js'
const serverFunctions: Record<string, ServerFunction> = {
'copy-data-from-locale': copyDataFromLocaleHandler,
'form-state': buildFormStateHandler,
'get-folder-results-component-and-data': getFolderResultsComponentAndDataHandler,
'render-document': renderDocumentHandler,
'render-document-slots': renderDocumentSlotsHandler,
'render-list': renderListHandler,
'schedule-publish': schedulePublishHandler,
'table-state': buildTableStateHandler,
}
export const handleServerFunctions: ServerFunctionHandler = async (args) => {
const { name: fnKey, args: fnArgs, config: configPromise, importMap } = args
@@ -26,18 +37,6 @@ export const handleServerFunctions: ServerFunctionHandler = async (args) => {
req,
}
const serverFunctions = {
'copy-data-from-locale': copyDataFromLocaleHandler as any as ServerFunction,
'form-state': buildFormStateHandler as any as ServerFunction,
'get-folder-results-component-and-data':
getFolderResultsComponentAndDataHandler as any as ServerFunction,
'render-document': renderDocumentHandler as any as ServerFunction,
'render-document-slots': renderDocumentSlotsHandler as any as ServerFunction,
'render-list': renderListHandler as any as ServerFunction,
'schedule-publish': schedulePublishHandler as any as ServerFunction,
'table-state': buildTableStateHandler as any as ServerFunction,
}
const fn = serverFunctions[fnKey]
if (!fn) {

View File

@@ -148,6 +148,7 @@ export async function Account({ initPageResult, params, searchParams }: AdminVie
importMap: payload.importMap,
serverProps: {
doc: data,
hasPublishedDoc,
i18n,
initPageResult,
locale,

View File

@@ -1,11 +1,5 @@
import type {
Data,
DocumentPreferences,
FormState,
Locale,
PayloadRequest,
VisibleEntities,
} from 'payload'
import type { RenderDocumentServerFunction } from '@payloadcms/ui'
import type { DocumentPreferences, VisibleEntities } from 'payload'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders } from 'next/headers.js'
@@ -13,26 +7,7 @@ import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { renderDocument } from './index.js'
type RenderDocumentResult = {
data: any
Document: React.ReactNode
preferences: DocumentPreferences
}
export const renderDocumentHandler = async (args: {
collectionSlug: string
disableActions?: boolean
docID: string
drawerSlug?: string
initialData?: Data
initialState?: FormState
locale?: Locale
overrideEntityVisibility?: boolean
redirectAfterCreate?: boolean
redirectAfterDelete: boolean
redirectAfterDuplicate: boolean
req: PayloadRequest
}): Promise<RenderDocumentResult> => {
export const renderDocumentHandler: RenderDocumentServerFunction = async (args) => {
const {
collectionSlug,
disableActions,
@@ -41,6 +16,7 @@ export const renderDocumentHandler = async (args: {
initialData,
locale,
overrideEntityVisibility,
paramsOverride,
redirectAfterCreate,
redirectAfterDelete,
redirectAfterDuplicate,
@@ -51,6 +27,8 @@ export const renderDocumentHandler = async (args: {
payload: { config },
user,
},
searchParams = {},
versions,
} = args
const headers = await getHeaders()
@@ -163,14 +141,15 @@ export const renderDocumentHandler = async (args: {
visibleEntities,
},
overrideEntityVisibility,
params: {
segments: ['collections', collectionSlug, docID],
params: paramsOverride ?? {
segments: ['collections', collectionSlug, String(docID)],
},
payload,
redirectAfterCreate,
redirectAfterDelete,
redirectAfterDuplicate,
searchParams: {},
searchParams,
versions,
viewType: 'document',
})

View File

@@ -6,6 +6,7 @@ import type {
DocumentViewServerPropsOnly,
EditViewComponent,
PayloadComponent,
RenderDocumentVersionsProperties,
} from 'payload'
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
@@ -57,6 +58,7 @@ export const renderDocument = async ({
redirectAfterDelete,
redirectAfterDuplicate,
searchParams,
versions,
viewType,
}: {
drawerSlug?: string
@@ -64,6 +66,7 @@ export const renderDocument = async ({
readonly redirectAfterCreate?: boolean
readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean
versions?: RenderDocumentVersionsProperties
} & AdminViewServerProps): Promise<{
data: Data
Document: React.ReactNode
@@ -178,6 +181,7 @@ export const renderDocument = async ({
const documentViewServerProps: DocumentViewServerPropsOnly = {
doc,
hasPublishedDoc,
i18n,
initPageResult,
locale,
@@ -187,6 +191,7 @@ export const renderDocument = async ({
routeSegments: segments,
searchParams,
user,
versions,
}
if (

View File

@@ -11,6 +11,7 @@ import type {
SanitizedGlobalConfig,
SaveButtonServerPropsOnly,
SaveDraftButtonServerPropsOnly,
ServerFunction,
ServerProps,
StaticDescription,
ViewDescriptionClientProps,
@@ -168,8 +169,8 @@ export const renderDocumentSlots: (args: {
return components
}
export const renderDocumentSlotsHandler = async (
args: { collectionSlug: string } & DefaultServerFunctionArgs,
export const renderDocumentSlotsHandler: ServerFunction<{ collectionSlug: string }> = async (
args,
) => {
const { collectionSlug, req } = args

View File

@@ -1,4 +1,4 @@
import type { ListPreferences, ListQuery, PayloadRequest, VisibleEntities } from 'payload'
import type { ListPreferences, ListQuery, ServerFunction, VisibleEntities } from 'payload'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders } from 'next/headers.js'
@@ -11,21 +11,23 @@ type RenderListResult = {
preferences: ListPreferences
}
export const renderListHandler = async (args: {
collectionSlug: string
disableActions?: boolean
disableBulkDelete?: boolean
disableBulkEdit?: boolean
disableQueryPresets?: boolean
documentDrawerSlug: string
drawerSlug?: string
enableRowSelections: boolean
overrideEntityVisibility?: boolean
query: ListQuery
redirectAfterDelete: boolean
redirectAfterDuplicate: boolean
req: PayloadRequest
}): Promise<RenderListResult> => {
export const renderListHandler: ServerFunction<
{
collectionSlug: string
disableActions?: boolean
disableBulkDelete?: boolean
disableBulkEdit?: boolean
disableQueryPresets?: boolean
documentDrawerSlug: string
drawerSlug?: string
enableRowSelections: boolean
overrideEntityVisibility?: boolean
query: ListQuery
redirectAfterDelete: boolean
redirectAfterDuplicate: boolean
},
Promise<RenderListResult>
> = async (args) => {
const {
collectionSlug,
disableActions,

View File

@@ -1,72 +1,74 @@
'use client'
import type { StepNavItem } from '@payloadcms/ui'
import type { ClientCollectionConfig, ClientField, ClientGlobalConfig } from 'payload'
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
import type React from 'react'
import { getTranslation } from '@payloadcms/translations'
import { useConfig, useLocale, useStepNav, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import { fieldAffectsData, formatAdminURL } from 'payload/shared'
import { useEffect } from 'react'
export const SetStepNav: React.FC<{
readonly collectionConfig?: ClientCollectionConfig
readonly collectionSlug?: string
readonly doc: any
readonly fields: ClientField[]
readonly globalConfig?: ClientGlobalConfig
readonly globalSlug?: string
readonly id?: number | string
}> = ({ id, collectionConfig, collectionSlug, doc, fields, globalConfig, globalSlug }) => {
versionToCreatedAtFormatted?: string
versionToID?: string
versionToUseAsTitle?: string
}> = ({
id,
collectionConfig,
globalConfig,
versionToCreatedAtFormatted,
versionToID,
versionToUseAsTitle,
}) => {
const { config } = useConfig()
const { setStepNav } = useStepNav()
const { i18n, t } = useTranslation()
const locale = useLocale()
useEffect(() => {
let nav: StepNavItem[] = []
const {
admin: { dateFormat },
routes: { admin: adminRoute },
} = config
if (collectionSlug && collectionConfig) {
let docLabel = ''
if (collectionConfig) {
const collectionSlug = collectionConfig.slug
const useAsTitle = collectionConfig?.admin?.useAsTitle || 'id'
const pluralLabel = collectionConfig?.labels?.plural
const formattedDoc = doc.version ? doc.version : doc
const useAsTitle = collectionConfig.admin?.useAsTitle || 'id'
const pluralLabel = collectionConfig.labels?.plural
let docLabel = `[${t('general:untitled')}]`
if (formattedDoc) {
if (useAsTitle !== 'id') {
const titleField = fields.find((f) => {
const fieldName = 'name' in f ? f.name : undefined
return Boolean(fieldAffectsData(f) && fieldName === useAsTitle)
})
const fields = collectionConfig.fields
if (titleField && formattedDoc[useAsTitle]) {
if ('localized' in titleField && titleField.localized) {
docLabel = formattedDoc[useAsTitle]?.[locale.code]
} else {
docLabel = formattedDoc[useAsTitle]
}
} else {
docLabel = `[${t('general:untitled')}]`
}
} else {
docLabel = doc.id
}
const titleField = fields.find(
(f) => fieldAffectsData(f) && 'name' in f && f.name === useAsTitle,
)
if (titleField && versionToUseAsTitle) {
docLabel =
'localized' in titleField && titleField.localized
? versionToUseAsTitle?.[locale.code] || docLabel
: versionToUseAsTitle
} else if (useAsTitle === 'id') {
docLabel = versionToID
}
nav = [
setStepNav([
{
label: getTranslation(pluralLabel, i18n),
url: formatAdminURL({ adminRoute, path: `/collections/${collectionSlug}` }),
url: formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}`,
}),
},
{
label: docLabel,
url: formatAdminURL({ adminRoute, path: `/collections/${collectionSlug}/${id}` }),
url: formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/${id}`,
}),
},
{
label: 'Versions',
@@ -76,51 +78,47 @@ export const SetStepNav: React.FC<{
}),
},
{
label: doc?.createdAt
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
: '',
label: versionToCreatedAtFormatted,
},
]
])
return
}
if (globalSlug && globalConfig) {
nav = [
if (globalConfig) {
const globalSlug = globalConfig.slug
setStepNav([
{
label: globalConfig.label,
url: formatAdminURL({
adminRoute,
path: `/globals/${globalConfig.slug}`,
path: `/globals/${globalSlug}`,
}),
},
{
label: 'Versions',
url: formatAdminURL({
adminRoute,
path: `/globals/${globalConfig.slug}/versions`,
path: `/globals/${globalSlug}/versions`,
}),
},
{
label: doc?.createdAt
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
: '',
label: versionToCreatedAtFormatted,
},
]
])
}
setStepNav(nav)
}, [
config,
setStepNav,
collectionSlug,
globalSlug,
doc,
id,
locale,
t,
i18n,
collectionConfig,
fields,
globalConfig,
versionToUseAsTitle,
versionToCreatedAtFormatted,
versionToID,
])
return null

View File

@@ -5,71 +5,165 @@
width: 100%;
padding-bottom: var(--spacing-view-bottom);
&__wrap {
padding-top: calc(var(--base) * 1.5);
display: flex;
flex-direction: column;
gap: var(--base);
}
&__header-wrap {
display: flex;
flex-direction: column;
gap: calc(var(--base) / 4);
}
&__header {
display: flex;
align-items: center;
flex-wrap: wrap;
h2 {
margin: 0;
}
}
&__created-at {
margin: 0;
&__toggle-locales-label {
color: var(--theme-elevation-500);
}
&__controls {
display: flex;
gap: var(--base);
&-controls-top {
border-bottom: 1px solid var(--theme-elevation-100);
padding: 16px var(--gutter-h) 16px var(--gutter-h);
> * {
flex-basis: 100%;
&__wrapper {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
&-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--base);
}
}
h2 {
font-size: 18px;
}
}
&-controls-bottom {
border-bottom: 1px solid var(--theme-elevation-100);
padding: 16px var(--gutter-h) 16px var(--gutter-h);
position: relative;
// Vertical separator line
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
background-color: var(--theme-elevation-100);
transform: translateX(-50%); // Center the line
}
&__wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: var(--base);
gap: var(--base);
}
}
&__time-elapsed {
color: var(--theme-elevation-500);
}
&__version-from {
display: flex;
flex-direction: column;
gap: 5px;
&-labels {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
&__version-to {
display: flex;
flex-direction: column;
gap: 5px;
&-labels {
display: flex;
flex-direction: row;
justify-content: space-between;
}
&-version {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background: var(--theme-elevation-50);
padding: 8px 12px;
gap: calc(var(--base) / 2);
h2 {
font-size: 13px;
font-weight: 400;
}
}
}
&__restore {
margin: 0 0 0 var(--base);
div {
margin-block: 0;
}
}
&__modifiedCheckBox {
margin: 0 0 0 var(--base);
display: flex;
align-items: center;
}
&__diff-wrap {
padding-top: var(--base);
display: flex;
flex-direction: column;
gap: var(--base);
position: relative;
// Vertical separator line
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
background-color: var(--theme-elevation-100);
transform: translateX(-50%); // Center the line
z-index: 2;
}
}
@include mid-break {
&__intro,
&__header {
display: block;
}
&__controls {
flex-direction: column;
gap: calc(var(--base) / 4);
}
&__restore {
margin: calc(var(--base) * 0.5) 0 0 0;
&__version-to {
&-version {
flex-direction: column;
align-items: flex-start;
}
}
}
@include small-break {
&__wrap {
&__diff-wrap {
padding-top: calc(var(--base) / 2);
gap: calc(var(--base) / 2);
}
&__version-to,
&__version-from {
&-labels {
flex-direction: column;
align-items: flex-start;
}
}
&-controls-top {
&__wrapper {
flex-direction: column;
align-items: flex-start;
.view-version__modifiedCheckBox {
margin-left: 0;
}
}
}
}
}

View File

@@ -1,24 +1,27 @@
'use client'
import type { OptionObject } from 'payload'
import {
CheckboxInput,
ChevronIcon,
formatTimeToNow,
Gutter,
Pill,
type SelectablePill,
useConfig,
useDocumentInfo,
useLocale,
useRouteTransition,
useTranslation,
} from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import { usePathname, useRouter, useSearchParams } from 'next/navigation.js'
import React, { useEffect, useMemo, useState } from 'react'
import React, { type FormEventHandler, useCallback, useEffect, useMemo, useState } from 'react'
import type { CompareOption, DefaultVersionsViewProps } from './types.js'
import Restore from '../Restore/index.js'
import { SelectComparison } from '../SelectComparison/index.js'
import { Restore } from '../Restore/index.js'
import './index.scss'
import { SelectLocales } from '../SelectLocales/index.js'
import { SelectComparison } from '../SelectComparison/index.js'
import { type SelectedLocaleOnChange, SelectLocales } from '../SelectLocales/index.js'
import { SelectedLocalesContext } from './SelectedLocalesContext.js'
import { SetStepNav } from './SetStepNav.js'
@@ -26,133 +29,163 @@ const baseClass = 'view-version'
export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
canUpdate,
doc,
latestDraftVersion,
latestPublishedVersion,
modifiedOnly: modifiedOnlyProp,
RenderedDiff,
selectedLocales: selectedLocalesProp,
versionID,
selectedLocales: selectedLocalesFromProps,
versionFromCreatedAt,
versionFromID,
versionFromOptions,
versionToCreatedAt,
versionToCreatedAtFormatted,
VersionToCreatedAtLabel,
versionToID,
versionToStatus,
versionToUseAsTitle,
}) => {
const { config, getEntityConfig } = useConfig()
const { code } = useLocale()
const { i18n, t } = useTranslation()
const availableLocales = useMemo(
() =>
config.localization
? config.localization.locales.map((locale) => ({
label: locale.label,
value: locale.code,
}))
: [],
[config.localization],
)
const [locales, setLocales] = useState<SelectablePill[]>([])
const [localeSelectorOpen, setLocaleSelectorOpen] = React.useState(false)
const { i18n } = useTranslation()
const { id, collectionSlug, globalSlug } = useDocumentInfo()
useEffect(() => {
if (config.localization) {
const updatedLocales = config.localization.locales.map((locale) => {
let label = locale.label
if (typeof locale.label !== 'string' && locale.label[code]) {
label = locale.label[code]
}
return {
name: locale.code,
Label: label,
selected: selectedLocalesFromProps.includes(locale.code),
} as SelectablePill
})
setLocales(updatedLocales)
}
}, [code, config.localization, selectedLocalesFromProps])
const { id: originalDocID, collectionSlug, globalSlug } = useDocumentInfo()
const { startRouteTransition } = useRouteTransition()
const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug }))
const { collectionConfig, globalConfig } = useMemo(() => {
return {
collectionConfig: getEntityConfig({ collectionSlug }),
globalConfig: getEntityConfig({ globalSlug }),
}
}, [collectionSlug, globalSlug, getEntityConfig])
const [globalConfig] = useState(() => getEntityConfig({ globalSlug }))
const [selectedLocales, setSelectedLocales] = useState<OptionObject[]>(selectedLocalesProp)
const [compareValue, setCompareValue] = useState<CompareOption>()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [modifiedOnly, setModifiedOnly] = useState(modifiedOnlyProp)
function onToggleModifiedOnly() {
setModifiedOnly(!modifiedOnly)
}
useEffect(() => {
// If the selected comparison doc or locales change, update URL params so that version page
// This is so that RSC can update the version comparison state
const current = new URLSearchParams(Array.from(searchParams.entries()))
const updateSearchParams = useCallback(
(args: {
modifiedOnly?: boolean
selectedLocales?: SelectablePill[]
versionFromID?: string
}) => {
// If the selected comparison doc or locales change, update URL params so that version page
// This is so that RSC can update the version comparison state
const current = new URLSearchParams(Array.from(searchParams.entries()))
if (!compareValue) {
current.delete('compareValue')
} else {
current.set('compareValue', compareValue?.value)
}
if (args?.versionFromID) {
current.set('versionFrom', args?.versionFromID)
}
if (!selectedLocales) {
current.delete('localeCodes')
} else {
current.set('localeCodes', JSON.stringify(selectedLocales.map((locale) => locale.value)))
}
if (args?.selectedLocales) {
if (!args.selectedLocales.length) {
current.delete('localeCodes')
} else {
const selectedLocaleCodes: string[] = []
for (const locale of args.selectedLocales) {
if (locale.selected) {
selectedLocaleCodes.push(locale.name)
}
}
current.set('localeCodes', JSON.stringify(selectedLocaleCodes))
}
}
if (modifiedOnly === false) {
current.set('modifiedOnly', 'false')
} else {
current.delete('modifiedOnly')
}
if (args?.modifiedOnly === false) {
current.set('modifiedOnly', 'false')
} else if (args?.modifiedOnly === true) {
current.delete('modifiedOnly')
}
const search = current.toString()
const query = search ? `?${search}` : ''
const search = current.toString()
const query = search ? `?${search}` : ''
// TODO: this transition occurs multiple times during the initial rendering phases, need to evaluate
startRouteTransition(() => router.push(`${pathname}${query}`))
}, [
compareValue,
pathname,
router,
searchParams,
selectedLocales,
modifiedOnly,
startRouteTransition,
])
startRouteTransition(() => router.push(`${pathname}${query}`))
},
[pathname, router, searchParams, startRouteTransition],
)
const {
admin: { dateFormat },
localization,
routes: { api: apiRoute },
serverURL,
} = config
const onToggleModifiedOnly: FormEventHandler<HTMLInputElement> = useCallback(
(event) => {
const newModified = (event.target as HTMLInputElement).checked
setModifiedOnly(newModified)
updateSearchParams({
modifiedOnly: newModified,
})
},
[updateSearchParams],
)
const versionCreatedAt = doc?.updatedAt
? formatDate({ date: doc.updatedAt, i18n, pattern: dateFormat })
: ''
const onChangeSelectedLocales: SelectedLocaleOnChange = useCallback(
({ locales }) => {
setLocales(locales)
updateSearchParams({
selectedLocales: locales,
})
},
[updateSearchParams],
)
const compareBaseURL = `${serverURL}${apiRoute}/${globalSlug ? 'globals/' : ''}${
collectionSlug || globalSlug
}/versions`
const onChangeVersionFrom: (val: CompareOption) => void = useCallback(
(val) => {
updateSearchParams({
versionFromID: val.value,
})
},
[updateSearchParams],
)
const draftsEnabled = Boolean((collectionConfig || globalConfig)?.versions.drafts)
const { localization } = config
const versionToTimeAgo = useMemo(
() =>
t('version:versionAgo', {
distance: formatTimeToNow({
date: versionToCreatedAt,
i18n,
}),
}),
[versionToCreatedAt, i18n, t],
)
const versionFromTimeAgo = useMemo(
() =>
versionFromCreatedAt
? t('version:versionAgo', {
distance: formatTimeToNow({
date: versionFromCreatedAt,
i18n,
}),
})
: undefined,
[versionFromCreatedAt, i18n, t],
)
return (
<main className={baseClass}>
<SetStepNav
collectionConfig={collectionConfig}
collectionSlug={collectionSlug}
doc={doc}
fields={(collectionConfig || globalConfig)?.fields}
globalConfig={globalConfig}
globalSlug={globalSlug}
id={id}
/>
<Gutter className={`${baseClass}__wrap`}>
<div className={`${baseClass}__header-wrap`}>
<p className={`${baseClass}__created-at`}>
{i18n.t('version:versionCreatedOn', {
version: i18n.t(doc?.autosave ? 'version:autosavedVersion' : 'version:version'),
})}
</p>
<header className={`${baseClass}__header`}>
<h2>{versionCreatedAt}</h2>
{canUpdate && (
<Restore
className={`${baseClass}__restore`}
collectionSlug={collectionSlug}
globalSlug={globalSlug}
label={collectionConfig?.labels.singular || globalConfig?.label}
originalDocID={id}
status={doc?.version?._status}
versionDate={versionCreatedAt}
versionID={versionID}
/>
)}
<Gutter className={`${baseClass}-controls-top`}>
<div className={`${baseClass}-controls-top__wrapper`}>
<h2>{i18n.t('version:compareVersions')}</h2>
<div className={`${baseClass}-controls-top__wrapper-actions`}>
<span className={`${baseClass}__modifiedCheckBox`}>
<CheckboxInput
checked={modifiedOnly}
@@ -161,31 +194,90 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
onToggle={onToggleModifiedOnly}
/>
</span>
</header>
{localization && (
<Pill
aria-controls={`${baseClass}-locales`}
aria-expanded={localeSelectorOpen}
className={`${baseClass}__toggle-locales`}
icon={<ChevronIcon direction={localeSelectorOpen ? 'up' : 'down'} />}
onClick={() => setLocaleSelectorOpen((localeSelectorOpen) => !localeSelectorOpen)}
pillStyle="light"
size="small"
>
<span className={`${baseClass}__toggle-locales-label`}>
{t('general:locales')}:{' '}
</span>
<span className={`${baseClass}__toggle-locales-list`}>
{locales
.filter((locale) => locale.selected)
.map((locale) => locale.name)
.join(', ')}
</span>
</Pill>
)}
</div>
</div>
<div className={`${baseClass}__controls`}>
<SelectComparison
baseURL={compareBaseURL}
draftsEnabled={draftsEnabled}
latestDraftVersion={latestDraftVersion}
latestPublishedVersion={latestPublishedVersion}
onChange={setCompareValue}
parentID={id}
value={compareValue}
versionID={versionID}
{localization && (
<SelectLocales
locales={locales}
localeSelectorOpen={localeSelectorOpen}
onChange={onChangeSelectedLocales}
/>
{localization && (
<SelectLocales
onChange={setSelectedLocales}
options={availableLocales}
value={selectedLocales}
)}
</Gutter>
<Gutter className={`${baseClass}-controls-bottom`}>
<div className={`${baseClass}-controls-bottom__wrapper`}>
<div className={`${baseClass}__version-from`}>
<div className={`${baseClass}__version-from-labels`}>
<span>{t('version:comparingAgainst')}</span>
{versionFromTimeAgo && (
<span className={`${baseClass}__time-elapsed`}>{versionFromTimeAgo}</span>
)}
</div>
<SelectComparison
collectionSlug={collectionSlug}
docID={originalDocID}
onChange={onChangeVersionFrom}
versionFromID={versionFromID}
versionFromOptions={versionFromOptions}
/>
)}
</div>
<div className={`${baseClass}__version-to`}>
<div className={`${baseClass}__version-to-labels`}>
<span>{t('version:currentlyViewing')}</span>
<span className={`${baseClass}__time-elapsed`}>{versionToTimeAgo}</span>
</div>
<div className={`${baseClass}__version-to-version`}>
{VersionToCreatedAtLabel}
{canUpdate && (
<Restore
className={`${baseClass}__restore`}
collectionConfig={collectionConfig}
globalConfig={globalConfig}
label={collectionConfig?.labels.singular || globalConfig?.label}
originalDocID={originalDocID}
status={versionToStatus}
versionDateFormatted={versionToCreatedAtFormatted}
versionID={versionToID}
/>
)}
</div>
</div>
</div>
<SelectedLocalesContext
value={{ selectedLocales: selectedLocales.map((locale) => locale.value) }}
>
{doc?.version && RenderedDiff}
</Gutter>
<SetStepNav
collectionConfig={collectionConfig}
globalConfig={globalConfig}
id={originalDocID}
versionToCreatedAtFormatted={versionToCreatedAtFormatted}
versionToID={versionToID}
versionToUseAsTitle={versionToUseAsTitle}
/>
<Gutter className={`${baseClass}__diff-wrap`}>
<SelectedLocalesContext value={{ selectedLocales: locales.map((locale) => locale.name) }}>
{versionToCreatedAt && RenderedDiff}
</SelectedLocalesContext>
</Gutter>
</main>

View File

@@ -1,19 +1,25 @@
import type { Document, OptionObject } from 'payload'
export type CompareOption = {
label: React.ReactNode | string
options?: CompareOption[]
relationTo?: string
value: string
}
export type DefaultVersionsViewProps = {
readonly canUpdate: boolean
readonly doc: Document
readonly latestDraftVersion?: string
readonly latestPublishedVersion?: string
modifiedOnly: boolean
readonly RenderedDiff: React.ReactNode
readonly selectedLocales: OptionObject[]
readonly versionID?: string
export type VersionPill = {
id: string
Label: React.ReactNode
}
export type DefaultVersionsViewProps = {
canUpdate: boolean
modifiedOnly: boolean
RenderedDiff: React.ReactNode
selectedLocales: string[]
versionFromCreatedAt?: string
versionFromID?: string
versionFromOptions: CompareOption[]
versionToCreatedAt?: string
versionToCreatedAtFormatted: string
VersionToCreatedAtLabel: React.ReactNode
versionToID?: string
versionToStatus?: string
versionToUseAsTitle?: string
}

View File

@@ -1,26 +1,61 @@
@import '~@payloadcms/ui/scss';
@layer payload-default {
.diff-collapser {
&__toggle-button {
all: unset;
cursor: pointer;
// Align the chevron visually with the label text
vertical-align: 1px;
position: relative;
z-index: 1;
display: flex;
align-items: center;
.icon {
color: var(--theme-elevation-500);
}
&:hover {
// Apply background color but with padding, thus we use after
&::before {
content: '';
position: absolute;
top: -(base(0.15));
left: -(base(0.15));
right: -(base(0.15));
bottom: -(base(0.15));
background-color: var(--theme-elevation-50);
border-radius: var(--style-radius-s);
z-index: -1;
}
.iterable-diff__label {
background-color: var(--theme-elevation-50);
z-index: 1;
}
}
}
&__label {
// Add space between label, chevron, and change count
margin: 0 calc(var(--base) * 0.25);
margin: 0 calc(var(--base) * 0.3) 0 0;
display: inline-flex;
height: 100%;
}
&__field-change-count {
// Reset the font weight of the change count to normal
font-weight: normal;
margin-left: calc(var(--base) * 0.3);
padding: calc(var(--base) * 0.1) calc(var(--base) * 0.2);
background: var(--theme-elevation-100);
border-radius: var(--style-radius-s);
font-size: 0.8rem;
}
&__content {
&__content:not(.diff-collapser__content--hide-gutter) {
[dir='ltr'] & {
// Vertical gutter
border-left: 3px solid var(--theme-elevation-50);
border-left: 2px solid var(--theme-elevation-100);
// Center-align the gutter with the chevron
margin-left: 3px;
// Content indentation
@@ -28,7 +63,7 @@
}
[dir='rtl'] & {
// Vertical gutter
border-right: 3px solid var(--theme-elevation-50);
border-right: 2px solid var(--theme-elevation-100);
// Center-align the gutter with the chevron
margin-right: 3px;
// Content indentation

View File

@@ -1,7 +1,7 @@
'use client'
import type { ClientField } from 'payload'
import { ChevronIcon, FieldDiffLabel, Pill, useConfig, useTranslation } from '@payloadcms/ui'
import { ChevronIcon, FieldDiffLabel, useConfig, useTranslation } from '@payloadcms/ui'
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
import React, { useState } from 'react'
@@ -10,45 +10,44 @@ import { countChangedFields, countChangedFieldsInRows } from '../utilities/count
const baseClass = 'diff-collapser'
type Props =
type Props = {
hideGutter?: boolean
initCollapsed?: boolean
Label: React.ReactNode
locales: string[] | undefined
parentIsLocalized: boolean
valueTo: unknown
} & (
| {
// fields collapser
children: React.ReactNode
comparison: unknown
field?: never
fields: ClientField[]
initCollapsed?: boolean
isIterable?: false
label: React.ReactNode
locales: string[] | undefined
parentIsLocalized: boolean
version: unknown
valueFrom: unknown
}
| {
// iterable collapser
children: React.ReactNode
comparison?: unknown
field: ClientField
fields?: never
initCollapsed?: boolean
isIterable: true
label: React.ReactNode
locales: string[] | undefined
parentIsLocalized: boolean
version: unknown
valueFrom?: unknown
}
)
export const DiffCollapser: React.FC<Props> = ({
children,
comparison,
field,
fields,
hideGutter = false,
initCollapsed = false,
isIterable = false,
label,
Label,
locales,
parentIsLocalized,
version,
valueFrom,
valueTo,
}) => {
const { t } = useTranslation()
const [isCollapsed, setIsCollapsed] = useState(initCollapsed)
@@ -62,8 +61,8 @@ export const DiffCollapser: React.FC<Props> = ({
'DiffCollapser: field must be an array or blocks field when isIterable is true',
)
}
const comparisonRows = comparison ?? []
const versionRows = version ?? []
const comparisonRows = valueFrom ?? []
const versionRows = valueTo ?? []
if (!Array.isArray(comparisonRows) || !Array.isArray(versionRows)) {
throw new Error(
@@ -81,18 +80,19 @@ export const DiffCollapser: React.FC<Props> = ({
})
} else {
changeCount = countChangedFields({
comparison,
comparison: valueFrom,
config,
fields,
locales,
parentIsLocalized,
version,
version: valueTo,
})
}
const contentClassNames = [
`${baseClass}__content`,
isCollapsed && `${baseClass}__content--is-collapsed`,
hideGutter && `${baseClass}__content--hide-gutter`,
]
.filter(Boolean)
.join(' ')
@@ -106,13 +106,14 @@ export const DiffCollapser: React.FC<Props> = ({
onClick={() => setIsCollapsed(!isCollapsed)}
type="button"
>
<ChevronIcon direction={isCollapsed ? 'right' : 'down'} />
<div className={`${baseClass}__label`}>{Label}</div>
<ChevronIcon direction={isCollapsed ? 'right' : 'down'} size={'small'} />
</button>
<span className={`${baseClass}__label`}>{label}</span>
{changeCount > 0 && (
<Pill className={`${baseClass}__field-change-count`} pillStyle="light-gray" size="small">
{changeCount > 0 && isCollapsed && (
<span className={`${baseClass}__field-change-count`}>
{t('version:changedFieldsCount', { count: changeCount })}
</Pill>
</span>
)}
</FieldDiffLabel>
<div className={contentClassNames}>{children}</div>

View File

@@ -8,8 +8,14 @@ import { ShimmerEffect } from '@payloadcms/ui'
import React, { Fragment, useEffect } from 'react'
export const RenderVersionFieldsToDiff = ({
parent = false,
versionFields,
}: {
/**
* If true, this is the parent render version fields component, not one nested in
* a field with children (e.g. group)
*/
parent?: boolean
versionFields: VersionField[]
}): React.ReactNode => {
const [hasMounted, setHasMounted] = React.useState(false)
@@ -21,7 +27,7 @@ export const RenderVersionFieldsToDiff = ({
}, [])
return (
<div className={baseClass}>
<div className={`${baseClass}${parent ? ` ${baseClass}--parent` : ''}`}>
{!hasMounted ? (
<Fragment>
<ShimmerEffect height="8rem" width="100%" />

View File

@@ -1,5 +1,4 @@
import type { I18nClient } from '@payloadcms/translations'
import type { DiffMethod } from 'react-diff-viewer-continued'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { dequal } from 'dequal/lite'
@@ -20,13 +19,11 @@ import {
} from 'payload'
import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared'
import { diffMethods } from './fields/diffMethods.js'
import { diffComponents } from './fields/index.js'
import { getFieldPathsModified } from './utilities/getFieldPathsModified.js'
export type BuildVersionFieldsArgs = {
clientSchemaMap: ClientFieldSchemaMap
comparisonSiblingData: object
customDiffComponents: Partial<
Record<FieldTypes, PayloadComponent<FieldDiffServerProps, FieldDiffClientProps>>
>
@@ -39,13 +36,15 @@ export type BuildVersionFieldsArgs = {
fields: Field[]
i18n: I18nClient
modifiedOnly: boolean
nestingLevel?: number
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
selectedLocales: string[]
versionSiblingData: object
versionFromSiblingData: object
versionToSiblingData: object
}
/**
@@ -57,20 +56,21 @@ export type BuildVersionFieldsArgs = {
*/
export const buildVersionFields = ({
clientSchemaMap,
comparisonSiblingData,
customDiffComponents,
entitySlug,
fieldPermissions,
fields,
i18n,
modifiedOnly,
nestingLevel = 0,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
selectedLocales,
versionSiblingData,
versionFromSiblingData,
versionToSiblingData,
}: BuildVersionFieldsArgs): {
versionFields: VersionField[]
} => {
@@ -112,9 +112,8 @@ export const buildVersionFields = ({
const fieldName: null | string = 'name' in field ? field.name : null
const versionValue = fieldName ? versionSiblingData?.[fieldName] : versionSiblingData
const comparisonValue = fieldName ? comparisonSiblingData?.[fieldName] : comparisonSiblingData
const valueFrom = fieldName ? versionFromSiblingData?.[fieldName] : versionFromSiblingData
const valueTo = fieldName ? versionToSiblingData?.[fieldName] : versionToSiblingData
if (isLocalized) {
versionField.fieldByLocale = {}
@@ -123,7 +122,6 @@ export const buildVersionFields = ({
const localizedVersionField = buildVersionField({
clientField: clientField as ClientField,
clientSchemaMap,
comparisonValue: comparisonValue?.[locale],
customDiffComponents,
entitySlug,
field,
@@ -132,6 +130,7 @@ export const buildVersionFields = ({
indexPath,
locale,
modifiedOnly,
nestingLevel,
parentIsLocalized: true,
parentPath,
parentSchemaPath,
@@ -139,7 +138,8 @@ export const buildVersionFields = ({
req,
schemaPath,
selectedLocales,
versionValue: versionValue?.[locale],
valueFrom: valueFrom?.[locale],
valueTo: valueTo?.[locale],
})
if (localizedVersionField) {
versionField.fieldByLocale[locale] = localizedVersionField
@@ -149,7 +149,6 @@ export const buildVersionFields = ({
const baseVersionField = buildVersionField({
clientField: clientField as ClientField,
clientSchemaMap,
comparisonValue,
customDiffComponents,
entitySlug,
field,
@@ -157,6 +156,7 @@ export const buildVersionFields = ({
i18n,
indexPath,
modifiedOnly,
nestingLevel,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath,
parentSchemaPath,
@@ -164,7 +164,8 @@ export const buildVersionFields = ({
req,
schemaPath,
selectedLocales,
versionValue,
valueFrom,
valueTo,
})
if (baseVersionField) {
@@ -172,7 +173,12 @@ export const buildVersionFields = ({
}
}
versionFields.push(versionField)
if (
versionField.field ||
(versionField.fieldByLocale && Object.keys(versionField.fieldByLocale).length)
) {
versionFields.push(versionField)
}
}
return {
@@ -183,7 +189,6 @@ export const buildVersionFields = ({
const buildVersionField = ({
clientField,
clientSchemaMap,
comparisonValue,
customDiffComponents,
entitySlug,
field,
@@ -192,6 +197,7 @@ const buildVersionField = ({
indexPath,
locale,
modifiedOnly,
nestingLevel,
parentIsLocalized,
parentPath,
parentSchemaPath,
@@ -199,26 +205,26 @@ const buildVersionField = ({
req,
schemaPath,
selectedLocales,
versionValue,
valueFrom,
valueTo,
}: {
clientField: ClientField
comparisonValue: unknown
field: Field
indexPath: string
locale?: string
modifiedOnly?: boolean
nestingLevel: number
parentIsLocalized: boolean
path: string
schemaPath: string
versionValue: unknown
valueFrom: unknown
valueTo: unknown
} & Omit<
BuildVersionFieldsArgs,
'comparisonSiblingData' | 'fields' | 'parentIndexPath' | 'versionSiblingData'
'fields' | 'parentIndexPath' | 'versionFromSiblingData' | 'versionToSiblingData'
>): BaseVersionField | null => {
const fieldName: null | string = 'name' in field ? field.name : null
const diffMethod: DiffMethod = diffMethods[field.type] || 'CHARS'
const hasPermission =
fieldPermissions === true ||
!fieldName ||
@@ -235,7 +241,7 @@ const buildVersionField = ({
return null
}
if (modifiedOnly && dequal(versionValue, comparisonValue)) {
if (modifiedOnly && dequal(valueFrom, valueTo)) {
return null
}
@@ -286,85 +292,110 @@ const buildVersionField = ({
parentPath,
parentSchemaPath,
})
baseVersionField.tabs.push({
const tabVersion = {
name: 'name' in tab ? tab.name : null,
fields: buildVersionFields({
clientSchemaMap,
comparisonSiblingData: 'name' in tab ? comparisonValue?.[tab.name] : comparisonValue,
customDiffComponents,
entitySlug,
fieldPermissions,
fields: tab.fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: isNamedTab ? '' : tabIndexPath,
parentIsLocalized: parentIsLocalized || tab.localized,
parentPath: isNamedTab ? tabPath : path,
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
req,
selectedLocales,
versionSiblingData: 'name' in tab ? versionValue?.[tab.name] : versionValue,
versionFromSiblingData: 'name' in tab ? valueFrom?.[tab.name] : valueFrom,
versionToSiblingData: 'name' in tab ? valueTo?.[tab.name] : valueTo,
}).versionFields,
label: tab.label,
})
}
if (tabVersion?.fields?.length) {
baseVersionField.tabs.push(tabVersion)
}
}
if (modifiedOnly && !baseVersionField.tabs.length) {
return null
}
} // At this point, we are dealing with a `row`, `collapsible`, etc
else if ('fields' in field) {
if (field.type === 'array' && versionValue) {
const arrayValue = Array.isArray(versionValue) ? versionValue : []
if (field.type === 'array' && (valueTo || valueFrom)) {
const maxLength = Math.max(
Array.isArray(valueTo) ? valueTo.length : 0,
Array.isArray(valueFrom) ? valueFrom.length : 0,
)
baseVersionField.rows = []
for (let i = 0; i < arrayValue.length; i++) {
const comparisonRow = comparisonValue?.[i] || {}
const versionRow = arrayValue?.[i] || {}
for (let i = 0; i < maxLength; i++) {
const fromRow = (Array.isArray(valueFrom) && valueFrom?.[i]) || {}
const toRow = (Array.isArray(valueTo) && valueTo?.[i]) || {}
baseVersionField.rows[i] = buildVersionFields({
clientSchemaMap,
comparisonSiblingData: comparisonRow,
customDiffComponents,
entitySlug,
fieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + i,
parentSchemaPath: schemaPath,
req,
selectedLocales,
versionSiblingData: versionRow,
versionFromSiblingData: fromRow,
versionToSiblingData: toRow,
}).versionFields
}
if (!baseVersionField.rows?.length && modifiedOnly) {
return null
}
} else {
baseVersionField.fields = buildVersionFields({
clientSchemaMap,
comparisonSiblingData: comparisonValue as object,
customDiffComponents,
entitySlug,
fieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
nestingLevel: field.type !== 'row' ? nestingLevel + 1 : nestingLevel,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath: 'name' in field ? path : parentPath,
parentSchemaPath: 'name' in field ? schemaPath : parentSchemaPath,
req,
selectedLocales,
versionSiblingData: versionValue as object,
versionFromSiblingData: valueFrom as object,
versionToSiblingData: valueTo as object,
}).versionFields
if (modifiedOnly && !baseVersionField.fields?.length) {
return null
}
}
} else if (field.type === 'blocks') {
baseVersionField.rows = []
const blocksValue = Array.isArray(versionValue) ? versionValue : []
const maxLength = Math.max(
Array.isArray(valueTo) ? valueTo.length : 0,
Array.isArray(valueFrom) ? valueFrom.length : 0,
)
for (let i = 0; i < blocksValue.length; i++) {
const comparisonRow = comparisonValue?.[i] || {}
const versionRow = blocksValue[i] || {}
for (let i = 0; i < maxLength; i++) {
const fromRow = (Array.isArray(valueFrom) && valueFrom?.[i]) || {}
const toRow = (Array.isArray(valueTo) && valueTo?.[i]) || {}
const blockSlugToMatch: string = versionRow.blockType
const versionBlock =
const blockSlugToMatch: string = toRow?.blockType ?? fromRow?.blockType
const toBlock =
req.payload.blocks[blockSlugToMatch] ??
((field.blockReferences ?? field.blocks).find(
(block) => typeof block !== 'string' && block.slug === blockSlugToMatch,
@@ -372,62 +403,77 @@ const buildVersionField = ({
let fields = []
if (versionRow.blockType === comparisonRow.blockType) {
fields = versionBlock.fields
if (toRow.blockType === fromRow.blockType) {
fields = toBlock.fields
} else {
const comparisonBlockSlugToMatch: string = versionRow.blockType
const fromBlockSlugToMatch: string = toRow?.blockType ?? fromRow?.blockType
const comparisonBlock =
req.payload.blocks[comparisonBlockSlugToMatch] ??
const fromBlock =
req.payload.blocks[fromBlockSlugToMatch] ??
((field.blockReferences ?? field.blocks).find(
(block) => typeof block !== 'string' && block.slug === comparisonBlockSlugToMatch,
(block) => typeof block !== 'string' && block.slug === fromBlockSlugToMatch,
) as FlattenedBlock | undefined)
if (comparisonBlock) {
fields = getUniqueListBy<Field>(
[...versionBlock.fields, ...comparisonBlock.fields],
'name',
)
if (fromBlock) {
fields = getUniqueListBy<Field>([...toBlock.fields, ...fromBlock.fields], 'name')
} else {
fields = versionBlock.fields
fields = toBlock.fields
}
}
baseVersionField.rows[i] = buildVersionFields({
clientSchemaMap,
comparisonSiblingData: comparisonRow,
customDiffComponents,
entitySlug,
fieldPermissions,
fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath: path + '.' + i,
parentSchemaPath: schemaPath + '.' + versionBlock.slug,
parentSchemaPath: schemaPath + '.' + toBlock.slug,
req,
selectedLocales,
versionSiblingData: versionRow,
versionFromSiblingData: fromRow,
versionToSiblingData: toRow,
}).versionFields
}
if (!baseVersionField.rows?.length && modifiedOnly) {
return null
}
}
const clientCellProps: FieldDiffClientProps = {
const clientDiffProps: FieldDiffClientProps = {
baseVersionField: {
...baseVersionField,
CustomComponent: undefined,
},
comparisonValue,
diffMethod,
/**
* TODO: Change to valueFrom in 4.0
*/
comparisonValue: valueFrom,
/**
* @deprecated remove in 4.0. Each field should handle its own diffing logic
*/
diffMethod: 'diffWordsWithSpace',
field: clientField,
fieldPermissions: subFieldPermissions,
parentIsLocalized,
versionValue,
nestingLevel: nestingLevel ? nestingLevel : undefined,
/**
* TODO: Change to valueTo in 4.0
*/
versionValue: valueTo,
}
if (locale) {
clientDiffProps.locale = locale
}
const serverCellProps: FieldDiffServerProps = {
...clientCellProps,
const serverDiffProps: FieldDiffServerProps = {
...clientDiffProps,
clientField,
field,
i18n,
@@ -436,22 +482,12 @@ const buildVersionField = ({
}
baseVersionField.CustomComponent = RenderServerComponent({
clientProps: locale
? ({
...clientCellProps,
locale,
} as FieldDiffClientProps)
: clientCellProps,
clientProps: clientDiffProps,
Component: CustomComponent,
Fallback: DefaultComponent,
importMap: req.payload.importMap,
key: 'diff component',
serverProps: locale
? ({
...serverCellProps,
locale,
} as FieldDiffServerProps)
: serverCellProps,
serverProps: serverDiffProps,
})
return baseVersionField

View File

@@ -13,10 +13,10 @@ const baseClass = 'collapsible-diff'
export const Collapsible: CollapsibleFieldDiffClientComponent = ({
baseVersionField,
comparisonValue,
comparisonValue: valueFrom,
field,
parentIsLocalized,
versionValue,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
@@ -28,16 +28,16 @@ export const Collapsible: CollapsibleFieldDiffClientComponent = ({
return (
<div className={baseClass}>
<DiffCollapser
comparison={comparisonValue}
fields={field.fields}
label={
Label={
'label' in field &&
field.label &&
typeof field.label !== 'function' && <span>{getTranslation(field.label, i18n)}</span>
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionValue}
valueFrom={valueFrom}
valueTo={valueTo}
>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />
</DiffCollapser>

View File

@@ -0,0 +1,12 @@
@layer payload-default {
.date-diff {
p *[data-match-type='delete'] {
color: unset !important;
background-color: unset !important;
}
p *[data-match-type='create'] {
color: unset !important;
background-color: unset !important;
}
}
}

View File

@@ -0,0 +1,73 @@
'use client'
import type { DateFieldDiffClientComponent } from 'payload'
import {
FieldDiffContainer,
getHTMLDiffComponents,
useConfig,
useTranslation,
} from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import './index.scss'
import React from 'react'
const baseClass = 'date-diff'
export const DateDiffComponent: DateFieldDiffClientComponent = ({
comparisonValue: valueFrom,
field,
locale,
nestingLevel,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
const {
config: {
admin: { dateFormat },
},
} = useConfig()
const formattedFromDate = valueFrom
? formatDate({
date: typeof valueFrom === 'string' ? new Date(valueFrom) : (valueFrom as Date),
i18n,
pattern: dateFormat,
})
: ''
const formattedToDate = valueTo
? formatDate({
date: typeof valueTo === 'string' ? new Date(valueTo) : (valueTo as Date),
i18n,
pattern: dateFormat,
})
: ''
const { From, To } = getHTMLDiffComponents({
fromHTML:
`<div class="${baseClass}" data-enable-match="true" data-date="${formattedFromDate}"><p>` +
formattedFromDate +
'</p></div>',
toHTML:
`<div class="${baseClass}" data-enable-match="true" data-date="${formattedToDate}"><p>` +
formattedToDate +
'</p></div>',
tokenizeByCharacter: false,
})
return (
<FieldDiffContainer
className={baseClass}
From={From}
i18n={i18n}
label={{
label: field.label,
locale,
}}
nestingLevel={nestingLevel}
To={To}
/>
)
}

View File

@@ -1,14 +1,4 @@
@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);
}
}
}
}

View File

@@ -16,11 +16,11 @@ const baseClass = 'group-diff'
export const Group: GroupFieldDiffClientComponent = ({
baseVersionField,
comparisonValue,
comparisonValue: valueFrom,
field,
locale,
parentIsLocalized,
versionValue,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
@@ -28,9 +28,8 @@ export const Group: GroupFieldDiffClientComponent = ({
return (
<div className={baseClass}>
<DiffCollapser
comparison={comparisonValue}
fields={field.fields}
label={
Label={
'label' in field &&
field.label &&
typeof field.label !== 'function' && (
@@ -42,7 +41,8 @@ export const Group: GroupFieldDiffClientComponent = ({
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionValue}
valueFrom={valueFrom}
valueTo={valueTo}
>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />
</DiffCollapser>

View File

@@ -1,8 +1,43 @@
@layer payload-default {
.iterable-diff {
&-label-container {
position: relative;
height: 20px;
display: flex;
flex-direction: row;
height: 100%;
}
&-label-prefix {
background-color: var(--theme-bg);
position: relative;
width: calc(var(--base) * 0.5);
height: 16px;
margin-left: calc((var(--base) * -0.5) - 5px);
margin-right: calc(var(--base) * 0.5);
&::before {
content: '';
position: absolute;
left: 1px;
top: 8px;
transform: translateY(-50%);
width: 6px;
height: 6px;
background-color: var(--theme-elevation-200);
border-radius: 50%;
margin-right: 5px;
}
}
&__label {
font-weight: 400;
color: var(--theme-elevation-600);
}
&__locale-label {
background: var(--theme-elevation-100);
padding: calc(var(--base) * 0.25);
border-radius: var(--style-radius-s);
padding: calc(var(--base) * 0.2);
// border-radius: $style-radius-m;
[dir='ltr'] & {
margin-right: calc(var(--base) * 0.25);
@@ -18,10 +53,7 @@
}
&__no-rows {
font-family: monospace;
background-color: var(--theme-elevation-50);
// padding: base(0.125) calc(var(--base) * 0.5);
// margin: base(0.125) 0;
color: var(--theme-elevation-400);
}
}
}

View File

@@ -19,18 +19,18 @@ const baseClass = 'iterable-diff'
export const Iterable: React.FC<FieldDiffClientProps> = ({
baseVersionField,
comparisonValue,
comparisonValue: valueFrom,
field,
locale,
parentIsLocalized,
versionValue,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
const { config } = useConfig()
const versionRowCount = Array.isArray(versionValue) ? versionValue.length : 0
const comparisonRowCount = Array.isArray(comparisonValue) ? comparisonValue.length : 0
const versionRowCount = Array.isArray(valueTo) ? valueTo.length : 0
const comparisonRowCount = Array.isArray(valueFrom) ? valueFrom.length : 0
const maxRows = Math.max(versionRowCount, comparisonRowCount)
if (!fieldIsArrayType(field) && !fieldIsBlockType(field)) {
@@ -40,10 +40,9 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
return (
<div className={baseClass}>
<DiffCollapser
comparison={comparisonValue}
field={field}
isIterable
label={
Label={
'label' in field &&
field.label &&
typeof field.label !== 'function' && (
@@ -55,13 +54,14 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized}
version={versionValue}
valueFrom={valueFrom}
valueTo={valueTo}
>
{maxRows > 0 && (
<div className={`${baseClass}__rows`}>
{Array.from(Array(maxRows).keys()).map((row, i) => {
const versionRow = versionValue?.[i] || {}
const comparisonRow = comparisonValue?.[i] || {}
const versionRow = valueTo?.[i] || {}
const comparisonRow = valueFrom?.[i] || {}
const { fields, versionFields } = getFieldsForRowComparison({
baseVersionField,
@@ -78,12 +78,18 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
return (
<div className={`${baseClass}__row`} key={i}>
<DiffCollapser
comparison={comparisonRow}
fields={fields}
label={rowLabel}
hideGutter={true}
Label={
<div className={`${baseClass}-label-container`}>
<div className={`${baseClass}-label-prefix`}></div>
<span className={`${baseClass}__label`}>{rowLabel}</span>
</div>
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionRow}
valueFrom={comparisonRow}
valueTo={versionRow}
>
<RenderVersionFieldsToDiff versionFields={versionFields} />
</DiffCollapser>

View File

@@ -0,0 +1,67 @@
import type { PayloadRequest, RelationshipField, TypeWithID } from 'payload'
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
import type { PopulatedRelationshipValue } from './index.js'
export const generateLabelFromValue = ({
field,
locale,
parentIsLocalized,
req,
value,
}: {
field: RelationshipField
locale: string
parentIsLocalized: boolean
req: PayloadRequest
value: PopulatedRelationshipValue
}): string => {
let relatedDoc: TypeWithID
let valueToReturn: string = ''
const relationTo: string = 'relationTo' in value ? value.relationTo : (field.relationTo as string)
if (typeof value === 'object' && 'relationTo' in value) {
relatedDoc = value.value
} else {
// Non-polymorphic relationship
relatedDoc = value
}
const relatedCollection = req.payload.collections[relationTo].config
const useAsTitle = relatedCollection?.admin?.useAsTitle
const useAsTitleField = relatedCollection.fields.find(
(f) => fieldAffectsData(f) && !fieldIsPresentationalOnly(f) && f.name === useAsTitle,
)
let titleFieldIsLocalized = false
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
titleFieldIsLocalized = fieldShouldBeLocalized({ field: useAsTitleField, parentIsLocalized })
}
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
valueToReturn = relatedDoc[useAsTitle]
} else {
valueToReturn = String(relatedDoc.id)
}
if (
typeof valueToReturn === 'object' &&
valueToReturn &&
titleFieldIsLocalized &&
valueToReturn?.[locale]
) {
valueToReturn = valueToReturn[locale]
}
if (
(valueToReturn && typeof valueToReturn === 'object' && valueToReturn !== null) ||
typeof valueToReturn !== 'string'
) {
valueToReturn = JSON.stringify(valueToReturn)
}
return valueToReturn
}

View File

@@ -1,15 +1,91 @@
@import '~@payloadcms/ui/scss';
@layer payload-default {
.relationship-diff-container .field-diff-content {
padding: 0;
background: unset;
}
.relationship-diff-container--hasOne {
.relationship-diff {
min-width: 100%;
max-width: fit-content;
}
}
.relationship-diff-container--hasMany .field-diff-content {
background: var(--theme-elevation-50);
padding: 10px;
.html-diff {
display: flex;
min-width: 0;
max-width: max-content;
flex-wrap: wrap;
gap: calc(var(--base) * 0.5);
}
.relationship-diff {
padding: calc(var(--base) * 0.15) calc(var(--base) * 0.3);
}
}
.relationship-diff {
&__locale-label {
[dir='ltr'] & {
margin-right: calc(var(--base) * 0.25);
@extend %body;
display: flex;
align-items: center;
border-radius: $style-radius-s;
border: 1px solid var(--theme-elevation-150);
position: relative;
font-family: var(--font-body);
max-height: calc(var(--base) * 3);
padding: calc(var(--base) * 0.35);
&[data-match-type='create'] {
border-color: var(--diff-create-pill-border);
color: var(--diff-create-parent-color);
* {
color: var(--diff-create-parent-color);
}
[dir='rtl'] & {
margin-left: calc(var(--base) * 0.25);
}
&[data-match-type='delete'] {
border-color: var(--diff-delete-pill-border);
color: var(--diff-delete-parent-color);
background-color: var(--diff-delete-pill-bg);
text-decoration-line: none !important;
* {
color: var(--diff-delete-parent-color);
text-decoration-line: none;
}
background: var(--theme-elevation-100);
padding: calc(var(--base) * 0.25);
// border-radius: $style-radius-m;
.relationship-diff__info {
text-decoration-line: line-through;
}
}
&__info {
font-weight: 500;
}
&__pill {
border-radius: $style-radius-s;
margin: 0 calc(var(--base) * 0.4) 0 calc(var(--base) * 0.2);
padding: 0 calc(var(--base) * 0.1);
background-color: var(--theme-elevation-150);
color: var(--theme-elevation-750);
}
&[data-match-type='create'] .relationship-diff__pill {
background-color: var(--diff-create-parent-bg);
color: var(--diff-create-pill-color);
}
&[data-match-type='delete'] .relationship-diff__pill {
background-color: var(--diff-delete-parent-bg);
color: var(--diff-delete-pill-color);
}
}
}

View File

@@ -1,185 +1,281 @@
'use client'
import type {
ClientCollectionConfig,
ClientConfig,
ClientField,
RelationshipFieldDiffClientComponent,
PayloadRequest,
RelationshipField,
RelationshipFieldDiffServerComponent,
TypeWithID,
} from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { FieldDiffLabel, useConfig, useTranslation } from '@payloadcms/ui'
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
import React from 'react'
import ReactDiffViewer from 'react-diff-viewer-continued'
import { getTranslation, type I18nClient } from '@payloadcms/translations'
import { FieldDiffContainer, getHTMLDiffComponents } from '@payloadcms/ui/rsc'
import './index.scss'
import { diffStyles } from '../styles.js'
import React from 'react'
import { generateLabelFromValue } from './generateLabelFromValue.js'
const baseClass = 'relationship-diff'
type RelationshipValue = Record<string, any>
export type PopulatedRelationshipValue = { relationTo: string; value: TypeWithID } | TypeWithID
const generateLabelFromValue = (
collections: ClientCollectionConfig[],
field: ClientField,
locale: string,
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
config: ClientConfig,
parentIsLocalized: boolean,
): string => {
if (Array.isArray(value)) {
return value
.map((v) => generateLabelFromValue(collections, field, locale, v, config, parentIsLocalized))
.filter(Boolean) // Filters out any undefined or empty values
.join(', ')
}
let relatedDoc: RelationshipValue
let valueToReturn: RelationshipValue | string = ''
const relationTo = 'relationTo' in field ? field.relationTo : undefined
if (value === null || typeof value === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- We want to return a string specifilly for null and undefined
return String(value)
}
if (typeof value === 'object' && 'relationTo' in value) {
relatedDoc = value.value
} else {
// Non-polymorphic relationship
relatedDoc = value
}
const relatedCollection = relationTo
? collections.find(
(c) =>
c.slug ===
(typeof value === 'object' && 'relationTo' in value ? value.relationTo : relationTo),
)
: null
if (relatedCollection) {
const useAsTitle = relatedCollection?.admin?.useAsTitle
const useAsTitleField = relatedCollection.fields.find(
(f) => fieldAffectsData(f) && !fieldIsPresentationalOnly(f) && f.name === useAsTitle,
)
let titleFieldIsLocalized = false
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
titleFieldIsLocalized = fieldShouldBeLocalized({ field: useAsTitleField, parentIsLocalized })
}
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
valueToReturn = relatedDoc[useAsTitle]
} else if (typeof relatedDoc?.id !== 'undefined') {
valueToReturn = relatedDoc.id
} else {
valueToReturn = relatedDoc
}
if (typeof valueToReturn === 'object' && titleFieldIsLocalized && valueToReturn?.[locale]) {
valueToReturn = valueToReturn[locale]
}
} else if (relatedDoc) {
// Handle non-polymorphic `hasMany` relationships or fallback
if (typeof relatedDoc?.id !== 'undefined') {
valueToReturn = String(relatedDoc.id)
} else {
valueToReturn = relatedDoc
}
}
if (
(valueToReturn && typeof valueToReturn === 'object' && valueToReturn !== null) ||
typeof valueToReturn !== 'string'
) {
valueToReturn = JSON.stringify(valueToReturn)
}
return valueToReturn
}
export const Relationship: RelationshipFieldDiffClientComponent = ({
comparisonValue,
export const Relationship: RelationshipFieldDiffServerComponent = ({
comparisonValue: valueFrom,
field,
i18n,
locale,
nestingLevel,
parentIsLocalized,
versionValue,
req,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
const { config } = useConfig()
const hasMany = 'hasMany' in field && field.hasMany
const polymorphic = Array.isArray(field.relationTo)
const placeholder = `[${i18n.t('general:noValue')}]`
const {
config: { collections },
} = useConfig()
let versionToRender: string | undefined = placeholder
let comparisonToRender: string | undefined = placeholder
if (versionValue) {
if ('hasMany' in field && field.hasMany && Array.isArray(versionValue)) {
versionToRender =
versionValue
.map((val) =>
generateLabelFromValue(collections, field, locale, val, config, parentIsLocalized),
)
.join(', ') || placeholder
} else {
versionToRender =
generateLabelFromValue(
collections,
field,
locale,
versionValue,
config,
parentIsLocalized,
) || placeholder
}
if (hasMany) {
return (
<ManyRelationshipDiff
field={field}
i18n={i18n}
locale={locale}
nestingLevel={nestingLevel}
parentIsLocalized={parentIsLocalized}
polymorphic={polymorphic}
req={req}
valueFrom={valueFrom as PopulatedRelationshipValue[] | undefined}
valueTo={valueTo as PopulatedRelationshipValue[] | undefined}
/>
)
}
if (comparisonValue) {
if ('hasMany' in field && field.hasMany && Array.isArray(comparisonValue)) {
comparisonToRender =
comparisonValue
.map((val) =>
generateLabelFromValue(collections, field, locale, val, config, parentIsLocalized),
)
.join(', ') || placeholder
} else {
comparisonToRender =
generateLabelFromValue(
collections,
field,
locale,
comparisonValue,
config,
parentIsLocalized,
) || placeholder
}
}
const label =
'label' in field && typeof field.label !== 'boolean' && typeof field.label !== 'function'
? field.label
: ''
return (
<div className={baseClass}>
<FieldDiffLabel>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{getTranslation(label, i18n)}
</FieldDiffLabel>
<ReactDiffViewer
hideLineNumbers
newValue={versionToRender}
oldValue={comparisonToRender}
showDiffOnly={false}
splitView
styles={diffStyles}
<SingleRelationshipDiff
field={field}
i18n={i18n}
locale={locale}
nestingLevel={nestingLevel}
parentIsLocalized={parentIsLocalized}
polymorphic={polymorphic}
req={req}
valueFrom={valueFrom as PopulatedRelationshipValue}
valueTo={valueTo as PopulatedRelationshipValue}
/>
)
}
export const SingleRelationshipDiff: React.FC<{
field: RelationshipField
i18n: I18nClient
locale: string
nestingLevel?: number
parentIsLocalized: boolean
polymorphic: boolean
req: PayloadRequest
valueFrom: PopulatedRelationshipValue
valueTo: PopulatedRelationshipValue
}> = async (args) => {
const {
field,
i18n,
locale,
nestingLevel,
parentIsLocalized,
polymorphic,
req,
valueFrom,
valueTo,
} = args
const ReactDOMServer = (await import('react-dom/server')).default
const FromComponent = valueFrom ? (
<RelationshipDocumentDiff
field={field}
i18n={i18n}
locale={locale}
parentIsLocalized={parentIsLocalized}
polymorphic={polymorphic}
relationTo={
polymorphic
? (valueFrom as { relationTo: string; value: TypeWithID }).relationTo
: (field.relationTo as string)
}
req={req}
showPill={true}
value={valueFrom}
/>
) : null
const ToComponent = valueTo ? (
<RelationshipDocumentDiff
field={field}
i18n={i18n}
locale={locale}
parentIsLocalized={parentIsLocalized}
polymorphic={polymorphic}
relationTo={
polymorphic
? (valueTo as { relationTo: string; value: TypeWithID }).relationTo
: (field.relationTo as string)
}
req={req}
showPill={true}
value={valueTo}
/>
) : null
const fromHTML = FromComponent ? ReactDOMServer.renderToStaticMarkup(FromComponent) : `<p></p>`
const toHTML = ToComponent ? ReactDOMServer.renderToStaticMarkup(ToComponent) : `<p></p>`
const diff = getHTMLDiffComponents({
fromHTML,
toHTML,
tokenizeByCharacter: false,
})
return (
<FieldDiffContainer
className={`${baseClass}-container ${baseClass}-container--hasOne`}
From={diff.From}
i18n={i18n}
label={{ label: field.label, locale }}
nestingLevel={nestingLevel}
To={diff.To}
/>
)
}
const ManyRelationshipDiff: React.FC<{
field: RelationshipField
i18n: I18nClient
locale: string
nestingLevel?: number
parentIsLocalized: boolean
polymorphic: boolean
req: PayloadRequest
valueFrom: PopulatedRelationshipValue[] | undefined
valueTo: PopulatedRelationshipValue[] | undefined
}> = async ({
field,
i18n,
locale,
nestingLevel,
parentIsLocalized,
polymorphic,
req,
valueFrom,
valueTo,
}) => {
const ReactDOMServer = (await import('react-dom/server')).default
const fromArr = Array.isArray(valueFrom) ? valueFrom : []
const toArr = Array.isArray(valueTo) ? valueTo : []
const makeNodes = (list: PopulatedRelationshipValue[]) =>
list.map((val, idx) => (
<RelationshipDocumentDiff
field={field}
i18n={i18n}
key={idx}
locale={locale}
parentIsLocalized={parentIsLocalized}
polymorphic={polymorphic}
relationTo={
polymorphic
? (val as { relationTo: string; value: TypeWithID }).relationTo
: (field.relationTo as string)
}
req={req}
showPill={polymorphic}
value={val}
/>
))
const fromNodes =
fromArr.length > 0 ? makeNodes(fromArr) : <p className={`${baseClass}__empty`}></p>
const toNodes = toArr.length > 0 ? makeNodes(toArr) : <p className={`${baseClass}__empty`}></p>
const fromHTML = ReactDOMServer.renderToStaticMarkup(fromNodes)
const toHTML = ReactDOMServer.renderToStaticMarkup(toNodes)
const diff = getHTMLDiffComponents({
fromHTML,
toHTML,
tokenizeByCharacter: false,
})
return (
<FieldDiffContainer
className={`${baseClass}-container ${baseClass}-container--hasMany`}
From={diff.From}
i18n={i18n}
label={{ label: field.label, locale }}
nestingLevel={nestingLevel}
To={diff.To}
/>
)
}
const RelationshipDocumentDiff = ({
field,
i18n,
locale,
parentIsLocalized,
polymorphic,
relationTo,
req,
showPill = false,
value,
}: {
field: RelationshipField
i18n: I18nClient
locale: string
parentIsLocalized: boolean
polymorphic: boolean
relationTo: string
req: PayloadRequest
showPill?: boolean
value: PopulatedRelationshipValue
}) => {
const localeToUse =
locale ??
(req.payload.config?.localization && req.payload.config?.localization?.defaultLocale) ??
'en'
const title = generateLabelFromValue({
field,
locale: localeToUse,
parentIsLocalized,
req,
value,
})
let pillLabel: null | string = null
if (showPill) {
const collectionConfig = req.payload.collections[relationTo].config
pillLabel = collectionConfig.labels?.singular
? getTranslation(collectionConfig.labels.singular, i18n)
: collectionConfig.slug
}
return (
<div
className={`${baseClass}`}
data-enable-match="true"
data-id={
polymorphic
? (value as { relationTo: string; value: TypeWithID }).value.id
: (value as TypeWithID).id
}
data-relation-to={relationTo}
>
{pillLabel && (
<span className={`${baseClass}__pill`} data-enable-match="false">
{pillLabel}
</span>
)}
<strong className={`${baseClass}__info`} data-enable-match="false">
{title}
</strong>
</div>
)
}

View File

@@ -1,23 +0,0 @@
'use client'
import React from 'react'
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
export const DiffViewer: React.FC<{
comparisonToRender: string
diffMethod: string
diffStyles: any
placeholder: string
versionToRender: string
}> = ({ comparisonToRender, diffMethod, diffStyles, placeholder, versionToRender }) => {
return (
<ReactDiffViewer
compareMethod={DiffMethod[diffMethod]}
hideLineNumbers
newValue={typeof versionToRender !== 'undefined' ? versionToRender : placeholder}
oldValue={comparisonToRender}
showDiffOnly={false}
splitView
styles={diffStyles}
/>
)
}

View File

@@ -1,15 +1,4 @@
@layer payload-default {
.select-diff {
&__locale-label {
[dir='ltr'] & {
margin-right: calc(var(--base) * 0.25);
}
[dir='rtl'] & {
margin-left: calc(var(--base) * 0.25);
}
background: var(--theme-elevation-100);
padding: calc(var(--base) * 0.25);
// border-radius: $style-radius-m;
}
}
}

View File

@@ -3,12 +3,10 @@ import type { I18nClient } from '@payloadcms/translations'
import type { Option, SelectField, SelectFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
import { FieldDiffContainer, getHTMLDiffComponents, useTranslation } from '@payloadcms/ui'
import React from 'react'
import './index.scss'
import { diffStyles } from '../styles.js'
import { DiffViewer } from './DiffViewer/index.js'
const baseClass = 'select-diff'
@@ -60,59 +58,58 @@ const getTranslatedOptions = (options: Option | Option[], i18n: I18nClient): str
}
export const Select: SelectFieldDiffClientComponent = ({
comparisonValue,
comparisonValue: valueFrom,
diffMethod,
field,
locale,
versionValue,
nestingLevel,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
let placeholder = ''
if (versionValue == comparisonValue) {
placeholder = `[${i18n.t('general:noValue')}]`
}
const options = 'options' in field && field.options
const comparisonToRender =
typeof comparisonValue !== 'undefined'
const renderedValueFrom =
typeof valueFrom !== 'undefined'
? getTranslatedOptions(
getOptionsToRender(
typeof comparisonValue === 'string' ? comparisonValue : JSON.stringify(comparisonValue),
typeof valueFrom === 'string' ? valueFrom : JSON.stringify(valueFrom),
options,
field.hasMany,
),
i18n,
)
: placeholder
: ''
const versionToRender =
typeof versionValue !== 'undefined'
const renderedValueTo =
typeof valueTo !== 'undefined'
? getTranslatedOptions(
getOptionsToRender(
typeof versionValue === 'string' ? versionValue : JSON.stringify(versionValue),
typeof valueTo === 'string' ? valueTo : JSON.stringify(valueTo),
options,
field.hasMany,
),
i18n,
)
: placeholder
: ''
const { From, To } = getHTMLDiffComponents({
fromHTML: '<p>' + renderedValueFrom + '</p>',
toHTML: '<p>' + renderedValueTo + '</p>',
tokenizeByCharacter: true,
})
return (
<div className={baseClass}>
<FieldDiffLabel>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{'label' in field && getTranslation(field.label || '', i18n)}
</FieldDiffLabel>
<DiffViewer
comparisonToRender={comparisonToRender}
diffMethod={diffMethod}
diffStyles={diffStyles}
placeholder={placeholder}
versionToRender={versionToRender}
/>
</div>
<FieldDiffContainer
className={baseClass}
From={From}
i18n={i18n}
label={{
label: field.label,
locale,
}}
nestingLevel={nestingLevel}
To={To}
/>
)
}

View File

@@ -5,16 +5,5 @@
&__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);
}
}
}
}

View File

@@ -19,7 +19,7 @@ import { RenderVersionFieldsToDiff } from '../../RenderVersionFieldsToDiff.js'
const baseClass = 'tabs-diff'
export const Tabs: TabsFieldDiffClientComponent = (props) => {
const { baseVersionField, comparisonValue, field, versionValue } = props
const { baseVersionField, comparisonValue: valueFrom, field, versionValue: valueTo } = props
const { selectedLocales } = useSelectedLocales()
return (
@@ -35,33 +35,32 @@ export const Tabs: TabsFieldDiffClientComponent = (props) => {
if ('name' in fieldTab && selectedLocales && fieldTab.localized) {
// Named localized tab
return selectedLocales.map((locale, index) => {
const localizedTabProps = {
const localizedTabProps: TabProps = {
...props,
comparison: comparisonValue?.[tab.name]?.[locale],
version: versionValue?.[tab.name]?.[locale],
comparisonValue: valueFrom?.[tab.name]?.[locale],
fieldTab,
locale,
tab,
versionValue: valueTo?.[tab.name]?.[locale],
}
return (
<div className={`${baseClass}__tab-locale`} key={[locale, index].join('-')}>
<div className={`${baseClass}__tab-locale-value`}>
<Tab
key={locale}
{...localizedTabProps}
fieldTab={fieldTab}
locale={locale}
tab={tab}
/>
<Tab key={locale} {...localizedTabProps} />
</div>
</div>
)
})
} else if ('name' in tab && tab.name) {
// Named tab
const namedTabProps = {
const namedTabProps: TabProps = {
...props,
comparison: comparisonValue?.[tab.name],
version: versionValue?.[tab.name],
comparisonValue: valueFrom?.[tab.name],
fieldTab,
tab,
versionValue: valueTo?.[tab.name],
}
return <Tab fieldTab={fieldTab} key={i} {...namedTabProps} tab={tab} />
return <Tab key={i} {...namedTabProps} />
} else {
// Unnamed tab
return <Tab fieldTab={fieldTab} key={i} {...props} tab={tab} />
@@ -80,12 +79,12 @@ type TabProps = {
} & FieldDiffClientProps<TabsFieldClient>
const Tab: React.FC<TabProps> = ({
comparisonValue,
comparisonValue: valueFrom,
fieldTab,
locale,
parentIsLocalized,
tab,
versionValue,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
@@ -96,9 +95,8 @@ const Tab: React.FC<TabProps> = ({
return (
<DiffCollapser
comparison={comparisonValue}
fields={fieldTab.fields}
label={
Label={
'label' in tab &&
tab.label &&
typeof tab.label !== 'function' && (
@@ -110,7 +108,8 @@ const Tab: React.FC<TabProps> = ({
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || fieldTab.localized}
version={versionValue}
valueFrom={valueFrom}
valueTo={valueTo}
>
<RenderVersionFieldsToDiff versionFields={tab.fields} />
</DiffCollapser>

View File

@@ -1,25 +0,0 @@
'use client'
import React from 'react'
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
export const DiffViewer: React.FC<{
comparisonToRender: string
diffMethod: string
diffStyles: any
placeholder: string
versionToRender: string
}> = ({ comparisonToRender, diffMethod, diffStyles, placeholder, versionToRender }) => {
return (
<ReactDiffViewer
compareMethod={DiffMethod[diffMethod]}
hideLineNumbers
newValue={typeof versionToRender !== 'undefined' ? String(versionToRender) : placeholder}
oldValue={
typeof comparisonToRender !== 'undefined' ? String(comparisonToRender) : placeholder
}
showDiffOnly={false}
splitView
styles={diffStyles}
/>
)
}

View File

@@ -1,15 +1,4 @@
@layer payload-default {
.text-diff {
&__locale-label {
[dir='ltr'] & {
margin-right: calc(var(--base) * 0.25);
}
[dir='rtl'] & {
margin-left: calc(var(--base) * 0.25);
}
background: var(--theme-elevation-100);
padding: calc(var(--base) * 0.25);
// border-radius: $style-radius-m;
}
}
}

View File

@@ -1,51 +1,92 @@
'use client'
import type { TextFieldDiffClientComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
import React from 'react'
import { FieldDiffContainer, getHTMLDiffComponents, useTranslation } from '@payloadcms/ui'
import './index.scss'
import { diffStyles } from '../styles.js'
import { DiffViewer } from './DiffViewer/index.js'
import React from 'react'
const baseClass = 'text-diff'
function formatValue(value: unknown): {
tokenizeByCharacter: boolean
value: string
} {
if (typeof value === 'string') {
return { tokenizeByCharacter: true, value }
}
if (typeof value === 'number') {
return {
tokenizeByCharacter: true,
value: String(value),
}
}
if (typeof value === 'boolean') {
return {
tokenizeByCharacter: false,
value: String(value),
}
}
if (value && typeof value === 'object') {
return {
tokenizeByCharacter: false,
value: `<pre>${JSON.stringify(value, null, 2)}</pre>`,
}
}
return {
tokenizeByCharacter: true,
value: undefined,
}
}
export const Text: TextFieldDiffClientComponent = ({
comparisonValue,
diffMethod,
comparisonValue: valueFrom,
field,
locale,
versionValue,
nestingLevel,
versionValue: valueTo,
}) => {
const { i18n } = useTranslation()
let placeholder = ''
if (versionValue == comparisonValue) {
placeholder = `[${i18n.t('general:noValue')}]`
if (valueTo == valueFrom) {
placeholder = `<span class="html-diff-no-value"><span>`
}
const versionToRender: string =
typeof versionValue === 'string' ? versionValue : JSON.stringify(versionValue, null, 2)
const comparisonToRender =
typeof comparisonValue === 'string' ? comparisonValue : JSON.stringify(comparisonValue, null, 2)
const formattedValueFrom = formatValue(valueFrom)
const formattedValueTo = formatValue(valueTo)
let tokenizeByCharacter = true
if (formattedValueFrom.value?.length) {
tokenizeByCharacter = formattedValueFrom.tokenizeByCharacter
} else if (formattedValueTo.value?.length) {
tokenizeByCharacter = formattedValueTo.tokenizeByCharacter
}
const renderedValueFrom = formattedValueFrom.value ?? placeholder
const renderedValueTo: string = formattedValueTo.value ?? placeholder
const { From, To } = getHTMLDiffComponents({
fromHTML: '<p>' + renderedValueFrom + '</p>',
toHTML: '<p>' + renderedValueTo + '</p>',
tokenizeByCharacter,
})
return (
<div className={baseClass}>
<FieldDiffLabel>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{'label' in field &&
typeof field.label !== 'function' &&
getTranslation(field.label || '', i18n)}
</FieldDiffLabel>
<DiffViewer
comparisonToRender={comparisonToRender}
diffMethod={diffMethod}
diffStyles={diffStyles}
placeholder={placeholder}
versionToRender={versionToRender}
/>
</div>
<FieldDiffContainer
className={baseClass}
From={From}
i18n={i18n}
label={{
label: field.label,
locale,
}}
nestingLevel={nestingLevel}
To={To}
/>
)
}

View File

@@ -0,0 +1,121 @@
@import '~@payloadcms/ui/scss';
@layer payload-default {
.upload-diff-container .field-diff-content {
padding: 0;
background: unset;
}
.upload-diff-hasMany {
display: flex;
flex-direction: column;
gap: calc(var(--base) * 0.4);
}
.upload-diff {
@extend %body;
min-width: 100%;
max-width: fit-content;
display: flex;
align-items: center;
background-color: var(--theme-elevation-50);
border-radius: $style-radius-s;
border: 1px solid var(--theme-elevation-150);
position: relative;
font-family: var(--font-body);
max-height: calc(var(--base) * 3);
padding: calc(var(--base) * 0.1);
&[data-match-type='create'] {
border-color: var(--diff-create-pill-border);
color: var(--diff-create-parent-color);
* {
color: var(--diff-create-parent-color);
}
.upload-diff__thumbnail {
border-radius: 0px;
border-color: var(--diff-create-pill-border);
background-color: none;
}
}
&[data-match-type='delete'] {
border-color: var(--diff-delete-pill-border);
text-decoration-line: none;
color: var(--diff-delete-parent-color);
background-color: var(--diff-delete-pill-bg);
* {
text-decoration-line: none;
color: var(--diff-delete-parent-color);
}
.upload-diff__thumbnail {
border-radius: 0px;
border-color: var(--diff-delete-pill-border);
background-color: none;
}
}
&__card {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
&__thumbnail {
width: calc(var(--base) * 3 - base(0.8) * 2);
height: calc(var(--base) * 3 - base(0.8) * 2);
position: relative;
overflow: hidden;
flex-shrink: 0;
border-radius: 0px;
border: 1px solid var(--theme-elevation-100);
img,
svg {
position: absolute;
object-fit: cover;
width: 100%;
height: 100%;
border-radius: 0px;
}
}
&__info {
flex-grow: 1;
display: flex;
align-items: flex-start;
flex-direction: column;
padding: calc(var(--base) * 0.25) calc(var(--base) * 0.6);
justify-content: space-between;
font-weight: 400;
strong {
font-weight: 500;
}
}
&__pill {
border-radius: $style-radius-s;
margin-left: calc(var(--base) * 0.6);
padding: 0 calc(var(--base) * 0.1);
background-color: var(--theme-elevation-150);
color: var(--theme-elevation-750);
}
&[data-match-type='create'] .upload-diff__pill {
background-color: var(--diff-create-parent-bg);
color: var(--diff-create-pill-color);
}
&[data-match-type='delete'] .upload-diff__pill {
background-color: var(--diff-delete-parent-bg);
color: var(--diff-delete-pill-color);
}
}
}

View File

@@ -0,0 +1,245 @@
import type {
FileData,
PayloadRequest,
TypeWithID,
UploadField,
UploadFieldDiffServerComponent,
} from 'payload'
import { getTranslation, type I18nClient } from '@payloadcms/translations'
import { FieldDiffContainer, File, getHTMLDiffComponents } from '@payloadcms/ui/rsc'
import './index.scss'
import React from 'react'
const baseClass = 'upload-diff'
export const Upload: UploadFieldDiffServerComponent = (args) => {
const {
comparisonValue: valueFrom,
field,
i18n,
locale,
nestingLevel,
req,
versionValue: valueTo,
} = args
if ('hasMany' in field && field.hasMany && Array.isArray(valueTo)) {
return (
<HasManyUploadDiff
field={field}
i18n={i18n}
locale={locale}
nestingLevel={nestingLevel}
req={req}
valueFrom={valueFrom as any}
valueTo={valueTo as any}
/>
)
}
return (
<SingleUploadDiff
field={field}
i18n={i18n}
locale={locale}
nestingLevel={nestingLevel}
req={req}
valueFrom={valueFrom as any}
valueTo={valueTo as any}
/>
)
}
export const HasManyUploadDiff: React.FC<{
field: UploadField
i18n: I18nClient
locale: string
nestingLevel?: number
req: PayloadRequest
valueFrom: Array<FileData & TypeWithID>
valueTo: Array<FileData & TypeWithID>
}> = async (args) => {
const { field, i18n, locale, nestingLevel, req, valueFrom, valueTo } = args
const ReactDOMServer = (await import('react-dom/server')).default
let From: React.ReactNode = ''
let To: React.ReactNode = ''
const showCollectionSlug = Array.isArray(field.relationTo)
const FromComponents = valueFrom
? valueFrom.map((uploadDoc) => (
<UploadDocumentDiff
i18n={i18n}
key={uploadDoc.id}
relationTo={field.relationTo}
req={req}
showCollectionSlug={showCollectionSlug}
uploadDoc={uploadDoc}
/>
))
: null
const ToComponents = valueTo
? valueTo.map((uploadDoc) => (
<UploadDocumentDiff
i18n={i18n}
key={uploadDoc.id}
relationTo={field.relationTo}
req={req}
showCollectionSlug={showCollectionSlug}
uploadDoc={uploadDoc}
/>
))
: null
const diffResult = getHTMLDiffComponents({
fromHTML:
`<div class="${baseClass}-hasMany">` +
(FromComponents
? FromComponents.map(
(component) => `<div>${ReactDOMServer.renderToStaticMarkup(component)}</div>`,
).join('')
: '') +
'</div>',
toHTML:
`<div class="${baseClass}-hasMany">` +
(ToComponents
? ToComponents.map(
(component) => `<div>${ReactDOMServer.renderToStaticMarkup(component)}</div>`,
).join('')
: '') +
'</div>',
tokenizeByCharacter: false,
})
From = diffResult.From
To = diffResult.To
return (
<FieldDiffContainer
className={`${baseClass}-container ${baseClass}-container--hasMany`}
From={From}
i18n={i18n}
label={{
label: field.label,
locale,
}}
nestingLevel={nestingLevel}
To={To}
/>
)
}
export const SingleUploadDiff: React.FC<{
field: UploadField
i18n: I18nClient
locale: string
nestingLevel?: number
req: PayloadRequest
valueFrom: FileData & TypeWithID
valueTo: FileData & TypeWithID
}> = async (args) => {
const { field, i18n, locale, nestingLevel, req, valueFrom, valueTo } = args
const ReactDOMServer = (await import('react-dom/server')).default
let From: React.ReactNode = ''
let To: React.ReactNode = ''
const showCollectionSlug = Array.isArray(field.relationTo)
const FromComponent = valueFrom ? (
<UploadDocumentDiff
i18n={i18n}
relationTo={field.relationTo}
req={req}
showCollectionSlug={showCollectionSlug}
uploadDoc={valueFrom}
/>
) : null
const ToComponent = valueTo ? (
<UploadDocumentDiff
i18n={i18n}
relationTo={field.relationTo}
req={req}
showCollectionSlug={showCollectionSlug}
uploadDoc={valueTo}
/>
) : null
const fromHtml = FromComponent
? ReactDOMServer.renderToStaticMarkup(FromComponent)
: '<p>' + '' + '</p>'
const toHtml = ToComponent
? ReactDOMServer.renderToStaticMarkup(ToComponent)
: '<p>' + '' + '</p>'
const diffResult = getHTMLDiffComponents({
fromHTML: fromHtml,
toHTML: toHtml,
tokenizeByCharacter: false,
})
From = diffResult.From
To = diffResult.To
return (
<FieldDiffContainer
className={`${baseClass}-container ${baseClass}-container--hasOne`}
From={From}
i18n={i18n}
label={{
label: field.label,
locale,
}}
nestingLevel={nestingLevel}
To={To}
/>
)
}
const UploadDocumentDiff = (args: {
i18n: I18nClient
relationTo: string
req: PayloadRequest
showCollectionSlug?: boolean
uploadDoc: FileData & TypeWithID
}) => {
const { i18n, relationTo, req, showCollectionSlug, uploadDoc } = args
const thumbnailSRC: string =
('thumbnailURL' in uploadDoc && (uploadDoc?.thumbnailURL as string)) || uploadDoc?.url || ''
let pillLabel: null | string = null
if (showCollectionSlug) {
const uploadConfig = req.payload.collections[relationTo].config
pillLabel = uploadConfig.labels?.singular
? getTranslation(uploadConfig.labels.singular, i18n)
: uploadConfig.slug
}
return (
<div
className={`${baseClass}`}
data-enable-match="true"
data-id={uploadDoc?.id}
data-relation-to={relationTo}
>
<div className={`${baseClass}__card`}>
<div className={`${baseClass}__thumbnail`}>
{thumbnailSRC?.length ? <img alt={uploadDoc?.filename} src={thumbnailSRC} /> : <File />}
</div>
{pillLabel && (
<div className={`${baseClass}__pill`} data-enable-match="false">
<span>{pillLabel}</span>
</div>
)}
<div className={`${baseClass}__info`} data-enable-match="false">
<strong>{uploadDoc?.filename}</strong>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +0,0 @@
export const diffMethods = {
radio: 'WORDS_WITH_SPACE',
relationship: 'WORDS_WITH_SPACE',
select: 'WORDS_WITH_SPACE',
upload: 'WORDS_WITH_SPACE',
}

View File

@@ -1,6 +1,7 @@
import type { FieldDiffClientProps, FieldTypes } from 'payload'
import type { FieldDiffClientProps, FieldDiffServerProps, FieldTypes } from 'payload'
import { Collapsible } from './Collapsible/index.js'
import { DateDiffComponent } from './Date/index.js'
import { Group } from './Group/index.js'
import { Iterable } from './Iterable/index.js'
import { Relationship } from './Relationship/index.js'
@@ -8,14 +9,18 @@ import { Row } from './Row/index.js'
import { Select } from './Select/index.js'
import { Tabs } from './Tabs/index.js'
import { Text } from './Text/index.js'
import { Upload } from './Upload/index.js'
export const diffComponents: Record<FieldTypes, React.ComponentType<FieldDiffClientProps>> = {
export const diffComponents: Record<
FieldTypes,
React.ComponentType<FieldDiffClientProps | FieldDiffServerProps>
> = {
array: Iterable,
blocks: Iterable,
checkbox: Text,
code: Text,
collapsible: Collapsible,
date: Text,
date: DateDiffComponent,
email: Text,
group: Group,
join: null,
@@ -31,5 +36,5 @@ export const diffComponents: Record<FieldTypes, React.ComponentType<FieldDiffCli
text: Text,
textarea: Text,
ui: null,
upload: Relationship,
upload: Upload,
}

View File

@@ -1,38 +0,0 @@
import type { ReactDiffViewerStylesOverride } from 'react-diff-viewer-continued'
export const diffStyles: ReactDiffViewerStylesOverride = {
diffContainer: {
minWidth: 'unset',
},
variables: {
dark: {
addedBackground: 'var(--theme-success-900)',
addedColor: 'var(--theme-success-100)',
diffViewerBackground: 'transparent',
diffViewerColor: 'var(--theme-text)',
emptyLineBackground: 'var(--theme-elevation-50)',
removedBackground: 'var(--theme-error-900)',
removedColor: 'var(--theme-error-100)',
wordAddedBackground: 'var(--theme-success-800)',
wordRemovedBackground: 'var(--theme-error-800)',
},
light: {
addedBackground: 'var(--theme-success-100)',
addedColor: 'var(--theme-success-900)',
diffViewerBackground: 'transparent',
diffViewerColor: 'var(--theme-text)',
emptyLineBackground: 'var(--theme-elevation-50)',
removedBackground: 'var(--theme-error-100)',
removedColor: 'var(--theme-error-900)',
wordAddedBackground: 'var(--theme-success-200)',
wordRemovedBackground: 'var(--theme-error-200)',
},
},
wordAdded: {
color: 'var(--theme-success-600)',
},
wordRemoved: {
color: 'var(--theme-error-600)',
textDecorationLine: 'line-through',
},
}

View File

@@ -4,5 +4,5 @@ import { RenderVersionFieldsToDiff } from './RenderVersionFieldsToDiff.js'
export const RenderDiff = (args: BuildVersionFieldsArgs): React.ReactNode => {
const { versionFields } = buildVersionFields(args)
return <RenderVersionFieldsToDiff versionFields={versionFields} />
return <RenderVersionFieldsToDiff parent={true} versionFields={versionFields} />
}

View File

@@ -4,6 +4,7 @@
.restore-version {
cursor: pointer;
display: flex;
min-width: max-content;
.popup-button {
display: flex;
@@ -24,7 +25,11 @@
}
}
&__button {
.btn {
margin-block: 0;
}
&__restore-as-draft-button {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
margin-right: 2px;

View File

@@ -1,5 +1,7 @@
'use client'
import type { ClientCollectionConfig, ClientGlobalConfig, SanitizedCollectionConfig } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import {
Button,
@@ -14,23 +16,33 @@ import {
import { requests } from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment, useCallback, useState } from 'react'
import type { Props } from './types.js'
import './index.scss'
import React, { Fragment, useCallback, useState } from 'react'
const baseClass = 'restore-version'
const modalSlug = 'restore-version'
const Restore: React.FC<Props> = ({
type Props = {
className?: string
collectionConfig?: ClientCollectionConfig
globalConfig?: ClientGlobalConfig
label: SanitizedCollectionConfig['labels']['singular']
originalDocID: number | string
status?: string
versionDateFormatted: string
versionID: string
}
export const Restore: React.FC<Props> = ({
className,
collectionSlug,
globalSlug,
collectionConfig,
globalConfig,
label,
originalDocID,
status,
versionDate,
versionDateFormatted,
versionID,
}) => {
const {
@@ -38,11 +50,8 @@ const Restore: React.FC<Props> = ({
routes: { admin: adminRoute, api: apiRoute },
serverURL,
},
getEntityConfig,
} = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug })
const { toggleModal } = useModal()
const router = useRouter()
const { i18n, t } = useTranslation()
@@ -51,31 +60,31 @@ const Restore: React.FC<Props> = ({
const restoreMessage = t('version:aboutToRestoreGlobal', {
label: getTranslation(label, i18n),
versionDate,
versionDate: versionDateFormatted,
})
let fetchURL = `${serverURL}${apiRoute}`
let redirectURL: string
const canRestoreAsDraft = status !== 'draft' && collectionConfig?.versions?.drafts
if (collectionSlug) {
fetchURL = `${fetchURL}/${collectionSlug}/versions/${versionID}?draft=${draft}`
redirectURL = formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/${originalDocID}`,
})
}
if (globalSlug) {
fetchURL = `${fetchURL}/globals/${globalSlug}/versions/${versionID}?draft=${draft}`
redirectURL = formatAdminURL({
adminRoute,
path: `/globals/${globalSlug}`,
})
}
const handleRestore = useCallback(async () => {
let fetchURL = `${serverURL}${apiRoute}`
let redirectURL: string
if (collectionConfig) {
fetchURL = `${fetchURL}/${collectionConfig.slug}/versions/${versionID}?draft=${draft}`
redirectURL = formatAdminURL({
adminRoute,
path: `/collections/${collectionConfig.slug}/${originalDocID}`,
})
}
if (globalConfig) {
fetchURL = `${fetchURL}/globals/${globalConfig.slug}/versions/${versionID}?draft=${draft}`
redirectURL = formatAdminURL({
adminRoute,
path: `/globals/${globalConfig.slug}`,
})
}
const res = await requests.post(fetchURL, {
headers: {
'Accept-Language': i18n.language,
@@ -89,16 +98,31 @@ const Restore: React.FC<Props> = ({
} else {
toast.error(t('version:problemRestoringVersion'))
}
}, [fetchURL, redirectURL, t, i18n, router, startRouteTransition])
}, [
serverURL,
apiRoute,
collectionConfig,
globalConfig,
i18n.language,
versionID,
draft,
adminRoute,
originalDocID,
startRouteTransition,
router,
t,
])
return (
<Fragment>
<div className={[baseClass, className].filter(Boolean).join(' ')}>
<Button
buttonStyle="pill"
className={[canRestoreAsDraft && `${baseClass}__button`].filter(Boolean).join(' ')}
buttonStyle="primary"
className={[canRestoreAsDraft && `${baseClass}__restore-as-draft-button`]
.filter(Boolean)
.join(' ')}
onClick={() => toggleModal(modalSlug)}
size="small"
size="xsmall"
SubMenuPopupContent={
canRestoreAsDraft
? () => (
@@ -124,5 +148,3 @@ const Restore: React.FC<Props> = ({
</Fragment>
)
}
export default Restore

View File

@@ -1,12 +0,0 @@
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
export type Props = {
className?: string
collectionSlug?: SanitizedCollectionConfig['slug']
globalSlug?: SanitizedGlobalConfig['slug']
label: SanitizedCollectionConfig['labels']['singular']
originalDocID: number | string
status?: string
versionDate: string
versionID: string
}

View File

@@ -0,0 +1,45 @@
'use client'
import { useConfig, useModal, useRouteTransition, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import { usePathname, useRouter, useSearchParams } from 'next/navigation.js'
import type { CreatedAtCellProps } from '../../../Versions/cells/CreatedAt/index.js'
export const VersionDrawerCreatedAtCell: React.FC<CreatedAtCellProps> = ({
rowData: { id, updatedAt } = {},
}) => {
const {
config: {
admin: { dateFormat },
},
} = useConfig()
const { closeAllModals } = useModal()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const { startRouteTransition } = useRouteTransition()
const { i18n } = useTranslation()
return (
<button
className="created-at-cell"
onClick={() => {
closeAllModals()
const current = new URLSearchParams(Array.from(searchParams.entries()))
if (id) {
current.set('versionFrom', String(id))
}
const search = current.toString()
const query = search ? `?${search}` : ''
startRouteTransition(() => router.push(`${pathname}${query}`))
}}
type="button"
>
{formatDate({ date: updatedAt, i18n, pattern: dateFormat })}
</button>
)
}

View File

@@ -0,0 +1,18 @@
@import '~@payloadcms/ui/scss';
@layer payload-default {
.version-drawer {
.table {
width: 100%;
}
.created-at-cell {
// Button reset, + underline
background: none;
border: none;
cursor: pointer;
padding: 0;
text-decoration: underline;
}
}
}

View File

@@ -0,0 +1,166 @@
'use client'
import {
Drawer,
LoadingOverlay,
toast,
useEditDepth,
useModal,
useServerFunctions,
useTranslation,
} from '@payloadcms/ui'
import { useSearchParams } from 'next/navigation.js'
import './index.scss'
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
export const baseClass = 'version-drawer'
export const formatVersionDrawerSlug = ({
depth,
uuid,
}: {
depth: number
uuid: string // supply when creating a new document and no id is available
}) => `version-drawer_${depth}_${uuid}`
export const VersionDrawerContent: React.FC<{
collectionSlug: string
docID: number | string
drawerSlug: string
}> = (props) => {
const { collectionSlug, docID, drawerSlug } = props
const { closeModal } = useModal()
const searchParams = useSearchParams()
const prevSearchParams = useRef(searchParams)
const { renderDocument } = useServerFunctions()
const [DocumentView, setDocumentView] = useState<React.ReactNode>(undefined)
const [isLoading, setIsLoading] = useState(true)
const hasRenderedDocument = useRef(false)
const { t } = useTranslation()
const getDocumentView = useCallback(
(docID?: number | string) => {
const fetchDocumentView = async () => {
setIsLoading(true)
try {
const result = await renderDocument({
collectionSlug,
docID,
drawerSlug,
paramsOverride: {
segments: ['collections', collectionSlug, String(docID), 'versions'],
},
redirectAfterDelete: false,
redirectAfterDuplicate: false,
searchParams: Object.fromEntries(searchParams.entries()),
versions: {
disableGutter: true,
useVersionDrawerCreatedAtCell: true,
},
})
if (result?.Document) {
setDocumentView(result.Document)
setIsLoading(false)
}
} catch (error) {
toast.error(error?.message || t('error:unspecific'))
closeModal(drawerSlug)
// toast.error(data?.errors?.[0].message || t('error:unspecific'))
}
}
void fetchDocumentView()
},
[closeModal, collectionSlug, drawerSlug, renderDocument, searchParams, t],
)
useEffect(() => {
if (!hasRenderedDocument.current || prevSearchParams.current !== searchParams) {
prevSearchParams.current = searchParams
getDocumentView(docID)
hasRenderedDocument.current = true
}
}, [docID, getDocumentView, searchParams])
if (isLoading) {
return <LoadingOverlay />
}
return DocumentView
}
export const VersionDrawer: React.FC<{
collectionSlug: string
docID: number | string
drawerSlug: string
}> = (props) => {
const { collectionSlug, docID, drawerSlug } = props
const { t } = useTranslation()
return (
<Drawer
className={baseClass}
gutter={true}
slug={drawerSlug}
title={t('version:selectVersionToCompare')}
>
<VersionDrawerContent collectionSlug={collectionSlug} docID={docID} drawerSlug={drawerSlug} />
</Drawer>
)
}
export const useVersionDrawer = ({
collectionSlug,
docID,
}: {
collectionSlug: string
docID: number | string
}) => {
const drawerDepth = useEditDepth()
const uuid = useId()
const { closeModal, modalState, openModal, toggleModal } = useModal()
const [isOpen, setIsOpen] = useState(false)
const drawerSlug = formatVersionDrawerSlug({
depth: drawerDepth,
uuid,
})
useEffect(() => {
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen))
}, [modalState, drawerSlug])
const toggleDrawer = useCallback(() => {
toggleModal(drawerSlug)
}, [toggleModal, drawerSlug])
const closeDrawer = useCallback(() => {
closeModal(drawerSlug)
}, [drawerSlug, closeModal])
const openDrawer = useCallback(() => {
openModal(drawerSlug)
}, [drawerSlug, openModal])
const MemoizedDrawer = useMemo(() => {
return () => (
<VersionDrawer collectionSlug={collectionSlug} docID={docID} drawerSlug={drawerSlug} />
)
}, [collectionSlug, docID, drawerSlug])
return useMemo(
() => ({
closeDrawer,
Drawer: MemoizedDrawer,
drawerDepth,
drawerSlug,
isDrawerOpen: isOpen,
openDrawer,
toggleDrawer,
}),
[MemoizedDrawer, closeDrawer, drawerDepth, drawerSlug, isOpen, openDrawer, toggleDrawer],
)
}

View File

@@ -1,15 +1,9 @@
@import '~@payloadcms/ui/scss';
@layer payload-default {
.compare-version {
&__error-loading {
border: 1px solid var(--theme-error-500);
min-height: calc(var(--base) * 2);
padding: calc(var(--base) * 0.5) calc(var(--base) * 0.75);
background-color: var(--theme-error-100);
color: var(--theme-elevation-0);
}
&__label {
margin-bottom: calc(var(--base) * 0.25);
&-moreVersions {
color: var(--theme-elevation-500);
}
}
}

View File

@@ -1,218 +1,67 @@
'use client'
import type { PaginatedDocs, Where } from 'payload'
import { fieldBaseClass, ReactSelect, useTranslation } from '@payloadcms/ui'
import React, { memo, useCallback, useMemo } from 'react'
import {
fieldBaseClass,
Pill,
ReactSelect,
useConfig,
useDocumentInfo,
useTranslation,
} from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import { stringify } from 'qs-esm'
import React, { useCallback, useEffect, useState } from 'react'
import type { CompareOption } from '../Default/types.js'
import './index.scss'
import type { Props } from './types.js'
import { renderPill } from '../../Versions/cells/AutosaveCell/index.js'
import './index.scss'
import { useVersionDrawer } from './VersionDrawer/index.js'
const baseClass = 'compare-version'
const maxResultsPerRequest = 10
const baseOptions = []
export const SelectComparison: React.FC<Props> = (props) => {
export const SelectComparison: React.FC<Props> = memo((props) => {
const {
baseURL,
draftsEnabled,
latestDraftVersion,
latestPublishedVersion,
onChange,
parentID,
value,
versionID,
collectionSlug,
docID,
onChange: onChangeFromProps,
versionFromID,
versionFromOptions,
} = props
const { t } = useTranslation()
const {
config: {
admin: { dateFormat },
localization,
},
} = useConfig()
const { Drawer, openDrawer } = useVersionDrawer({ collectionSlug, docID })
const { hasPublishedDoc } = useDocumentInfo()
const options = useMemo(() => {
return [
...versionFromOptions,
{
label: <span className={`${baseClass}-moreVersions`}>{t('version:moreVersions')}</span>,
value: 'more',
},
]
}, [t, versionFromOptions])
const [options, setOptions] = useState<
{
label: React.ReactNode | string
value: string
}[]
>(baseOptions)
const [lastLoadedPage, setLastLoadedPage] = useState(1)
const [errorLoading, setErrorLoading] = useState('')
const { i18n, t } = useTranslation()
const loadedAllOptionsRef = React.useRef(false)
const currentOption = useMemo(
() => versionFromOptions.find((option) => option.value === versionFromID),
[versionFromOptions, versionFromID],
)
const getResults = useCallback(
async ({ lastLoadedPage: lastLoadedPageArg }) => {
if (loadedAllOptionsRef.current) {
const onChange = useCallback(
(val: CompareOption) => {
if (val.value === 'more') {
openDrawer()
return
}
const query: {
[key: string]: unknown
where: Where
} = {
depth: 0,
limit: maxResultsPerRequest,
page: lastLoadedPageArg,
where: {
and: [
{
id: {
not_equals: versionID,
},
},
],
},
}
if (parentID) {
query.where.and.push({
parent: {
equals: parentID,
},
})
}
if (localization && draftsEnabled) {
query.where.and.push({
snapshot: {
not_equals: true,
},
})
}
const search = stringify(query)
const response = await fetch(`${baseURL}?${search}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
})
if (response.ok) {
const data: PaginatedDocs = await response.json()
if (data.docs.length > 0) {
const versionInfo = {
draft: {
currentLabel: t('version:currentDraft'),
latestVersion: latestDraftVersion,
pillStyle: undefined,
previousLabel: t('version:draft'),
},
published: {
currentLabel: t('version:currentPublishedVersion'),
// The latest published version does not necessarily equal the current published version,
// because the latest published version might have been unpublished in the meantime.
// Hence, we should only use the latest published version if there is a published document.
latestVersion: hasPublishedDoc ? latestPublishedVersion : undefined,
pillStyle: 'success',
previousLabel: t('version:previouslyPublished'),
},
}
const additionalOptions = data.docs.map((doc) => {
const status = doc.version._status
let publishedLocalePill = null
const publishedLocale = doc.publishedLocale || undefined
const { currentLabel, latestVersion, pillStyle, previousLabel } =
versionInfo[status] || {}
if (localization && localization?.locales && publishedLocale) {
const localeCode = Array.isArray(publishedLocale)
? publishedLocale[0]
: publishedLocale
const locale = localization.locales.find((loc) => loc.code === localeCode)
const formattedLabel = locale?.label?.[i18n?.language] || locale?.label
if (formattedLabel) {
publishedLocalePill = <Pill size="small">{formattedLabel}</Pill>
}
}
return {
label: (
<div>
{formatDate({ date: doc.updatedAt, i18n, pattern: dateFormat })}
&nbsp;&nbsp;
{renderPill(doc, latestVersion, currentLabel, previousLabel, pillStyle)}
{publishedLocalePill}
</div>
),
value: doc.id,
}
})
setOptions((existingOptions) => [...existingOptions, ...additionalOptions])
if (!data.hasNextPage) {
loadedAllOptionsRef.current = true
}
setLastLoadedPage(data.page)
}
} else {
setErrorLoading(t('error:unspecific'))
}
onChangeFromProps(val)
},
[dateFormat, baseURL, parentID, versionID, t, i18n, latestDraftVersion, latestPublishedVersion],
[onChangeFromProps, openDrawer],
)
useEffect(() => {
if (!i18n.dateFNS) {
// If dateFNS is not loaded, we can't format the date in getResults
return
}
void getResults({ lastLoadedPage: 1 })
}, [getResults, i18n.dateFNS])
const filteredOptions = options.filter(
(option, index, self) => self.findIndex((t) => t.value === option.value) === index,
)
useEffect(() => {
if (filteredOptions.length > 0 && !value) {
onChange(filteredOptions[0])
}
}, [filteredOptions, value, onChange])
return (
<div
className={[fieldBaseClass, baseClass, errorLoading && 'error-loading']
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__label`}>{t('version:compareVersion')}</div>
{!errorLoading && (
<ReactSelect
isClearable={false}
isSearchable={false}
onChange={onChange}
onMenuScrollToBottom={() => {
void getResults({ lastLoadedPage: lastLoadedPage + 1 })
}}
options={filteredOptions}
placeholder={t('version:selectVersionToCompare')}
value={value}
/>
)}
{errorLoading && <div className={`${baseClass}__error-loading`}>{errorLoading}</div>}
<div className={[fieldBaseClass, baseClass].filter(Boolean).join(' ')}>
<ReactSelect
isClearable={false}
isSearchable={false}
onChange={onChange}
options={options}
placeholder={t('version:selectVersionToCompare')}
value={currentOption}
/>
<Drawer />
</div>
)
}
})

View File

@@ -3,14 +3,11 @@ import type { PaginatedDocs, SanitizedCollectionConfig } from 'payload'
import type { CompareOption } from '../Default/types.js'
export type Props = {
baseURL: string
draftsEnabled?: boolean
latestDraftVersion?: string
latestPublishedVersion?: string
collectionSlug: string
docID: number | string
onChange: (val: CompareOption) => void
parentID?: number | string
value: CompareOption
versionID: string
versionFromID?: string
versionFromOptions: CompareOption[]
}
type CLEAR = {

View File

@@ -1,9 +0,0 @@
@layer payload-default {
.select-version-locales {
flex-grow: 1;
&__label {
margin-bottom: calc(var(--base) * 0.25);
}
}
}

View File

@@ -1,41 +1,41 @@
'use client'
import { ReactSelect, useLocale, useTranslation } from '@payloadcms/ui'
import { AnimateHeight } from '@payloadcms/ui'
import { PillSelector, type SelectablePill } from '@payloadcms/ui'
import React from 'react'
import type { Props } from './types.js'
import './index.scss'
const baseClass = 'select-version-locales'
export const SelectLocales: React.FC<Props> = ({ onChange, options, value }) => {
const { t } = useTranslation()
const { code } = useLocale()
const format = (items) => {
return items.map((item) => {
if (typeof item.label === 'string') {
return item
}
if (typeof item.label !== 'string' && item.label[code]) {
return {
label: item.label[code],
value: item.value,
}
}
})
}
export type SelectedLocaleOnChange = (args: { locales: SelectablePill[] }) => void
export type Props = {
locales: SelectablePill[]
localeSelectorOpen: boolean
onChange: SelectedLocaleOnChange
}
export const SelectLocales: React.FC<Props> = ({ locales, localeSelectorOpen, onChange }) => {
return (
<div className={baseClass}>
<div className={`${baseClass}__label`}>{t('version:showLocales')}</div>
<ReactSelect
isMulti
onChange={onChange}
options={format(options)}
placeholder={t('version:selectLocales')}
value={format(value)}
<AnimateHeight
className={baseClass}
height={localeSelectorOpen ? 'auto' : 0}
id={`${baseClass}-locales`}
>
<PillSelector
onClick={({ pill }) => {
const newLocales = locales.map((locale) => {
if (locale.name === pill.name) {
return {
...locale,
selected: !pill.selected,
}
} else {
return locale
}
})
onChange({ locales: newLocales })
}}
pills={locales}
/>
</div>
</AnimateHeight>
)
}

View File

@@ -1,7 +0,0 @@
import type { OptionObject } from 'payload'
export type Props = {
onChange: (options: OptionObject[]) => void
options: OptionObject[]
value: OptionObject[]
}

View File

@@ -0,0 +1,122 @@
'use client'
import { Pill, useConfig, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import React from 'react'
import './index.scss'
import { getVersionLabel } from './getVersionLabel.js'
const baseClass = 'version-pill-label'
const renderPill = (label: React.ReactNode, pillStyle: Parameters<typeof Pill>[0]['pillStyle']) => {
return (
<Pill pillStyle={pillStyle} size="small">
{label}
</Pill>
)
}
export const VersionPillLabel: React.FC<{
currentlyPublishedVersion?: {
id: number | string
updatedAt: string
}
disableDate?: boolean
doc: {
[key: string]: unknown
id: number | string
publishedLocale?: string
updatedAt?: string
version: {
[key: string]: unknown
_status: string
}
}
/**
* By default, the date is displayed first, followed by the version label.
* @default false
*/
labelFirst?: boolean
labelOverride?: React.ReactNode
/**
* @default 'pill'
*/
labelStyle?: 'pill' | 'text'
labelSuffix?: React.ReactNode
latestDraftVersion?: {
id: number | string
updatedAt: string
}
}> = ({
currentlyPublishedVersion,
disableDate = false,
doc,
labelFirst = false,
labelOverride,
labelStyle = 'pill',
labelSuffix,
latestDraftVersion,
}) => {
const {
config: {
admin: { dateFormat },
localization,
},
} = useConfig()
const { i18n, t } = useTranslation()
const { label, pillStyle } = getVersionLabel({
currentlyPublishedVersion,
latestDraftVersion,
t,
version: doc,
})
const labelText: React.ReactNode = (
<span>
{labelOverride || label}
{labelSuffix}
</span>
)
const showDate = !disableDate && doc.updatedAt
const formattedDate = showDate
? formatDate({ date: doc.updatedAt, i18n, pattern: dateFormat })
: null
const localeCode = Array.isArray(doc.publishedLocale)
? doc.publishedLocale[0]
: doc.publishedLocale
const locale =
localization && localization?.locales
? localization.locales.find((loc) => loc.code === localeCode)
: null
const localeLabel = locale ? locale?.label?.[i18n?.language] || locale?.label : null
return (
<div className={baseClass}>
{labelFirst ? (
<React.Fragment>
{labelStyle === 'pill' ? (
renderPill(labelText, pillStyle)
) : (
<span className={`${baseClass}-text`}>{labelText}</span>
)}
{showDate && <span className={`${baseClass}-date`}>{formattedDate}</span>}
</React.Fragment>
) : (
<React.Fragment>
{showDate && <span className={`${baseClass}-date`}>{formattedDate}</span>}
{labelStyle === 'pill' ? (
renderPill(labelText, pillStyle)
) : (
<span className={`${baseClass}-text`}>{labelText}</span>
)}
</React.Fragment>
)}
{localeLabel && <Pill>{localeLabel}</Pill>}
</div>
)
}

View File

@@ -0,0 +1,62 @@
import type { TFunction } from '@payloadcms/translations'
import type { Pill } from '@payloadcms/ui'
type Args = {
currentlyPublishedVersion?: {
id: number | string
updatedAt: string
}
latestDraftVersion?: {
id: number | string
updatedAt: string
}
t: TFunction
version: {
id: number | string
version: { _status?: string }
}
}
/**
* Gets the appropriate version label and version pill styling
* given existing versions and the current version status.
*/
export function getVersionLabel({
currentlyPublishedVersion,
latestDraftVersion,
t,
version,
}: Args): {
label: string
name: 'currentDraft' | 'currentlyPublished' | 'draft' | 'previouslyPublished' | 'published'
pillStyle: Parameters<typeof Pill>[0]['pillStyle']
} {
const publishedNewerThanDraft =
currentlyPublishedVersion?.updatedAt > latestDraftVersion?.updatedAt
if (version.version._status === 'draft') {
if (publishedNewerThanDraft) {
return {
name: 'draft',
label: t('version:draft'),
pillStyle: 'light',
}
} else {
return {
name: version.id === latestDraftVersion?.id ? 'currentDraft' : 'draft',
label:
version.id === latestDraftVersion?.id ? t('version:currentDraft') : t('version:draft'),
pillStyle: 'light',
}
}
} else {
const isCurrentlyPublished = version.id === currentlyPublishedVersion?.id
return {
name: isCurrentlyPublished ? 'currentlyPublished' : 'previouslyPublished',
label: isCurrentlyPublished
? t('version:currentlyPublished')
: t('version:previouslyPublished'),
pillStyle: isCurrentlyPublished ? 'success' : 'light',
}
}
}

View File

@@ -0,0 +1,26 @@
@import '~@payloadcms/ui/scss';
@layer payload-default {
.version-pill-label {
display: flex;
align-items: center;
gap: calc(var(--base) / 2);
&-text {
font-weight: 500;
}
&-date {
color: var(--theme-elevation-500);
}
}
@include small-break {
.version-pill-label {
// Column
flex-direction: column;
align-items: flex-start;
gap: 0;
}
}
}

View File

@@ -0,0 +1,192 @@
import {
logError,
type PaginatedDocs,
type PayloadRequest,
type SelectType,
type Sort,
type TypeWithVersion,
type User,
type Where,
} from 'payload'
export const fetchVersion = async <TVersionData extends object = object>({
id,
collectionSlug,
depth,
globalSlug,
locale,
overrideAccess,
req,
select,
user,
}: {
collectionSlug?: string
depth?: number
globalSlug?: string
id: number | string
locale?: 'all' | ({} & string)
overrideAccess?: boolean
req: PayloadRequest
select?: SelectType
user?: User
}): Promise<null | TypeWithVersion<TVersionData>> => {
try {
if (collectionSlug) {
return (await req.payload.findVersionByID({
id: String(id),
collection: collectionSlug,
depth,
locale,
overrideAccess,
req,
select,
user,
})) as TypeWithVersion<TVersionData>
} else if (globalSlug) {
return (await req.payload.findGlobalVersionByID({
id: String(id),
slug: globalSlug,
depth,
locale,
overrideAccess,
req,
select,
user,
})) as TypeWithVersion<TVersionData>
}
} catch (err) {
logError({ err, payload: req.payload })
return null
}
}
export const fetchVersions = async <TVersionData extends object = object>({
collectionSlug,
depth,
draft,
globalSlug,
limit,
locale,
overrideAccess,
page,
parentID,
req,
select,
sort,
user,
where: whereFromArgs,
}: {
collectionSlug?: string
depth?: number
draft?: boolean
globalSlug?: string
limit?: number
locale?: 'all' | ({} & string)
overrideAccess?: boolean
page?: number
parentID?: number | string
req: PayloadRequest
select?: SelectType
sort?: Sort
user?: User
where?: Where
}): Promise<null | PaginatedDocs<TypeWithVersion<TVersionData>>> => {
const where: Where = { and: [...(whereFromArgs ? [whereFromArgs] : [])] }
try {
if (collectionSlug) {
if (parentID) {
where.and.push({
parent: {
equals: parentID,
},
})
}
return (await req.payload.findVersions({
collection: collectionSlug,
depth,
draft,
limit,
locale,
overrideAccess,
page,
req,
select,
sort,
user,
where,
})) as PaginatedDocs<TypeWithVersion<TVersionData>>
} else if (globalSlug) {
return (await req.payload.findGlobalVersions({
slug: globalSlug,
depth,
limit,
locale,
overrideAccess,
page,
req,
select,
sort,
user,
where,
})) as PaginatedDocs<TypeWithVersion<TVersionData>>
}
} catch (err) {
logError({ err, payload: req.payload })
return null
}
}
export const fetchLatestVersion = async <TVersionData extends object = object>({
collectionSlug,
depth,
globalSlug,
locale,
overrideAccess,
parentID,
req,
select,
status,
user,
where,
}: {
collectionSlug?: string
depth?: number
globalSlug?: string
locale?: 'all' | ({} & string)
overrideAccess?: boolean
parentID?: number | string
req: PayloadRequest
select?: SelectType
status: 'draft' | 'published'
user?: User
where?: Where
}): Promise<null | TypeWithVersion<TVersionData>> => {
const and: Where[] = [
{
'version._status': {
equals: status,
},
},
...(where ? [where] : []),
]
const latest = await fetchVersions({
collectionSlug,
depth,
draft: true,
globalSlug,
limit: 1,
locale,
overrideAccess,
parentID,
req,
select,
sort: '-updatedAt',
user,
where: { and },
})
return latest?.docs?.length ? (latest.docs[0] as TypeWithVersion<TVersionData>) : null
}

View File

@@ -1,24 +1,28 @@
import type {
Document,
DocumentViewServerProps,
Locale,
OptionObject,
SanitizedCollectionPermission,
SanitizedGlobalPermission,
TypeWithVersion,
} from 'payload'
import { formatDate } from '@payloadcms/ui/shared'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { getClientSchemaMap } from '@payloadcms/ui/utilities/getClientSchemaMap'
import { getSchemaMap } from '@payloadcms/ui/utilities/getSchemaMap'
import { notFound } from 'next/navigation.js'
import React from 'react'
import { getLatestVersion } from '../Versions/getLatestVersion.js'
import type { CompareOption } from './Default/types.js'
import { DefaultVersionView } from './Default/index.js'
import { fetchLatestVersion, fetchVersion, fetchVersions } from './fetchVersions.js'
import { RenderDiff } from './RenderFieldsToDiff/index.js'
import { getVersionLabel } from './VersionPillLabel/getVersionLabel.js'
import { VersionPillLabel } from './VersionPillLabel/VersionPillLabel.js'
export async function VersionView(props: DocumentViewServerProps) {
const { i18n, initPageResult, routeSegments, searchParams } = props
const { hasPublishedDoc, i18n, initPageResult, routeSegments, searchParams } = props
const {
collectionConfig,
@@ -26,125 +30,159 @@ export async function VersionView(props: DocumentViewServerProps) {
globalConfig,
permissions,
req,
req: { payload, payload: { config } = {}, user } = {},
req: { payload, payload: { config, config: { localization } } = {}, user } = {},
} = initPageResult
const versionID = routeSegments[routeSegments.length - 1]
const versionToID = routeSegments[routeSegments.length - 1]
const collectionSlug = collectionConfig?.slug
const globalSlug = globalConfig?.slug
const draftsEnabled = (collectionConfig ?? globalConfig)?.versions?.drafts
const localeCodesFromParams = searchParams.localeCodes
? JSON.parse(searchParams.localeCodes as string)
: null
const comparisonVersionIDFromParams: string = searchParams.compareValue as string
const versionFromIDFromParams = searchParams.versionFrom as string
const modifiedOnly: boolean = searchParams.modifiedOnly === 'false' ? false : true
const { localization } = config
const docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission = collectionSlug
? permissions.collections[collectionSlug]
: permissions.globals[globalSlug]
let docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
let slug: string
const versionTo = await fetchVersion<{
_status?: string
}>({
id: versionToID,
collectionSlug,
depth: 1,
globalSlug,
locale: 'all',
overrideAccess: false,
req,
user,
})
let doc: Document
let latestPublishedVersion = null
let latestDraftVersion = null
if (!versionTo) {
return notFound()
}
if (collectionSlug) {
// /collections/:slug/:id/versions/:versionID
slug = collectionSlug
docPermissions = permissions.collections[collectionSlug]
try {
doc = await payload.findVersionByID({
id: versionID,
collection: slug,
depth: 0,
locale: 'all',
overrideAccess: false,
req,
user,
})
if (collectionConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion({
slug,
type: 'collection',
const [
previousVersionResult,
versionFromResult,
currentlyPublishedVersion,
latestDraftVersion,
previousPublishedVersionResult,
] = await Promise.all([
// Previous version (the one before the versionTo)
fetchVersions({
collectionSlug,
// If versionFromIDFromParams is provided, the previous version is only used in the version comparison dropdown => depth 0 is enough.
// If it's not provided, this is used as `versionFrom` in the comparison, which expects populated data => depth 1 is needed.
depth: versionFromIDFromParams ? 0 : 1,
draft: true,
globalSlug,
limit: 1,
locale: 'all',
overrideAccess: false,
parentID: id,
req,
sort: '-updatedAt',
user,
where: {
and: [
{
updatedAt: {
less_than: versionTo.updatedAt,
},
},
],
},
}),
// Version from ID from params
(versionFromIDFromParams
? fetchVersion({
id: versionFromIDFromParams,
collectionSlug,
depth: 1,
globalSlug,
locale: 'all',
overrideAccess: false,
req,
user,
})
: Promise.resolve(null)) as Promise<null | TypeWithVersion<object>>,
// Currently published version - do note: currently published != latest published, as an unpublished version can be the latest published
hasPublishedDoc
? fetchLatestVersion({
collectionSlug,
depth: 0,
globalSlug,
locale: 'all',
overrideAccess: false,
parentID: id,
payload,
req,
status: 'draft',
status: 'published',
user,
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'collection',
: Promise.resolve(null),
// Latest draft version
draftsEnabled
? fetchLatestVersion({
collectionSlug,
depth: 0,
globalSlug,
locale: 'all',
overrideAccess: false,
parentID: id,
payload,
req,
status: 'published',
})
}
} catch (_err) {
return notFound()
}
}
if (globalSlug) {
// /globals/:slug/versions/:versionID
slug = globalSlug
docPermissions = permissions.globals[globalSlug]
try {
doc = await payload.findGlobalVersionByID({
id: versionID,
slug,
depth: 0,
locale: 'all',
overrideAccess: false,
req,
user,
})
if (globalConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion({
slug,
type: 'global',
locale: 'all',
overrideAccess: false,
payload,
req,
status: 'draft',
user,
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'global',
locale: 'all',
overrideAccess: false,
payload,
req,
status: 'published',
})
}
} catch (_err) {
return notFound()
}
}
: Promise.resolve(null),
// Previous published version
fetchVersions({
collectionSlug,
depth: 0,
draft: true,
globalSlug,
limit: 1,
locale: 'all',
overrideAccess: false,
parentID: id,
req,
sort: '-updatedAt',
user,
where: {
and: [
{
updatedAt: {
less_than: versionTo.updatedAt,
},
},
{
'version._status': {
equals: 'published',
},
},
],
},
}),
])
const publishedNewerThanDraft = latestPublishedVersion?.updatedAt > latestDraftVersion?.updatedAt
const previousVersion: null | TypeWithVersion<object> = previousVersionResult?.docs?.[0] ?? null
if (publishedNewerThanDraft) {
latestDraftVersion = {
id: '',
updatedAt: '',
}
}
const versionFrom =
versionFromResult ||
// By default, we'll compare the previous version. => versionFrom = version previous to versionTo
previousVersion
let selectedLocales: OptionObject[] = []
// Previous published version before the versionTo
const previousPublishedVersion = previousPublishedVersionResult?.docs?.[0] ?? null
let selectedLocales: string[] = []
if (localization) {
let locales: Locale[] = []
if (localeCodesFromParams) {
@@ -163,48 +201,7 @@ export async function VersionView(props: DocumentViewServerProps) {
locales = (await localization.filterAvailableLocales({ locales, req })) || []
}
selectedLocales = locales.map((locale) => ({
label: locale.label,
value: locale.code,
}))
}
const latestVersion =
latestPublishedVersion?.updatedAt > latestDraftVersion?.updatedAt
? latestPublishedVersion
: latestDraftVersion
if (!doc) {
return notFound()
}
/**
* The doc to compare this version to is either the latest version, or a specific version if specified in the URL.
* This specific version is added to the URL when a user selects a version to compare to.
*/
let comparisonDoc = null
if (comparisonVersionIDFromParams) {
if (collectionSlug) {
comparisonDoc = await payload.findVersionByID({
id: comparisonVersionIDFromParams,
collection: collectionSlug,
depth: 0,
locale: 'all',
overrideAccess: false,
req,
})
} else {
comparisonDoc = await payload.findGlobalVersionByID({
id: comparisonVersionIDFromParams,
slug: globalSlug,
depth: 0,
locale: 'all',
overrideAccess: false,
req,
})
}
} else {
comparisonDoc = latestVersion
selectedLocales = locales.map((locale) => locale.code)
}
const schemaMap = getSchemaMap({
@@ -222,10 +219,8 @@ export async function VersionView(props: DocumentViewServerProps) {
payload,
schemaMap,
})
const RenderedDiff = RenderDiff({
clientSchemaMap,
comparisonSiblingData: comparisonDoc?.version,
customDiffComponents: {},
entitySlug: collectionSlug || globalSlug,
fieldPermissions: docPermissions?.fields,
@@ -237,26 +232,200 @@ export async function VersionView(props: DocumentViewServerProps) {
parentPath: '',
parentSchemaPath: '',
req,
selectedLocales: selectedLocales && selectedLocales.map((locale) => locale.value),
versionSiblingData: globalConfig
? {
...doc?.version,
createdAt: doc?.version?.createdAt || doc.createdAt,
updatedAt: doc?.version?.updatedAt || doc.updatedAt,
}
: doc?.version,
selectedLocales,
versionFromSiblingData: {
...versionFrom?.version,
updatedAt: versionFrom?.updatedAt,
},
versionToSiblingData: {
...versionTo.version,
updatedAt: versionTo.updatedAt,
},
})
const versionToCreatedAtFormatted = versionTo.updatedAt
? formatDate({
date:
typeof versionTo.updatedAt === 'string'
? new Date(versionTo.updatedAt)
: (versionTo.updatedAt as Date),
i18n,
pattern: config.admin.dateFormat,
})
: ''
const formatPill = ({
doc,
labelOverride,
labelStyle,
labelSuffix,
}: {
doc: TypeWithVersion<any>
labelOverride?: string
labelStyle?: 'pill' | 'text'
labelSuffix?: React.ReactNode
}): React.ReactNode => {
return (
<VersionPillLabel
currentlyPublishedVersion={currentlyPublishedVersion}
doc={doc}
key={doc.id}
labelFirst={true}
labelOverride={labelOverride}
labelStyle={labelStyle ?? 'text'}
labelSuffix={labelSuffix}
latestDraftVersion={latestDraftVersion}
/>
)
}
// SelectComparison Options:
//
// Previous version: always, unless doesn't exist. Can be the same as previously published
// Latest draft: only if no newer published exists (latestDraftVersion)
// Currently published: always, if exists
// Previously published: if there is a prior published version older than versionTo
// Specific Version: only if not already present under other label (= versionFrom)
let versionFromOptions: {
doc: TypeWithVersion<any>
labelOverride?: string
updatedAt: Date
value: string
}[] = []
// Previous version
if (previousVersion?.id) {
versionFromOptions.push({
doc: previousVersion,
labelOverride: i18n.t('version:previousVersion'),
updatedAt: new Date(previousVersion.updatedAt),
value: previousVersion.id,
})
}
// Latest Draft
const publishedNewerThanDraft =
currentlyPublishedVersion?.updatedAt > latestDraftVersion?.updatedAt
if (latestDraftVersion && !publishedNewerThanDraft) {
versionFromOptions.push({
doc: latestDraftVersion,
updatedAt: new Date(latestDraftVersion.updatedAt),
value: latestDraftVersion.id,
})
}
// Currently Published
if (currentlyPublishedVersion) {
versionFromOptions.push({
doc: currentlyPublishedVersion,
updatedAt: new Date(currentlyPublishedVersion.updatedAt),
value: currentlyPublishedVersion.id,
})
}
// Previous Published
if (previousPublishedVersion && currentlyPublishedVersion?.id !== previousPublishedVersion.id) {
versionFromOptions.push({
doc: previousPublishedVersion,
labelOverride: i18n.t('version:previouslyPublished'),
updatedAt: new Date(previousPublishedVersion.updatedAt),
value: previousPublishedVersion.id,
})
}
// Specific Version
if (versionFrom?.id && !versionFromOptions.some((option) => option.value === versionFrom.id)) {
// Only add "specific version" if it is not already in the options
versionFromOptions.push({
doc: versionFrom,
labelOverride: i18n.t('version:specificVersion'),
updatedAt: new Date(versionFrom.updatedAt),
value: versionFrom.id,
})
}
versionFromOptions = versionFromOptions.sort((a, b) => {
// Sort by updatedAt, newest first
if (a && b) {
return b.updatedAt.getTime() - a.updatedAt.getTime()
}
return 0
})
const versionToIsVersionFrom = versionFrom?.id === versionTo.id
const versionFromComparisonOptions: CompareOption[] = []
for (const option of versionFromOptions) {
const isVersionTo = option.value === versionTo.id
if (isVersionTo && !versionToIsVersionFrom) {
// Don't offer selecting a versionFrom that is the same as versionTo, unless it's already selected
continue
}
const alreadyAdded = versionFromComparisonOptions.some(
(existingOption) => existingOption.value === option.value,
)
if (alreadyAdded) {
continue
}
const otherOptionsWithSameID = versionFromOptions.filter(
(existingOption) => existingOption.value === option.value && existingOption !== option,
)
// Merge options with same ID to the same option
const labelSuffix = otherOptionsWithSameID?.length ? (
<span key={`${option.value}-suffix`}>
{' ('}
{otherOptionsWithSameID.map((optionWithSameID, index) => {
const label =
optionWithSameID.labelOverride ||
getVersionLabel({
currentlyPublishedVersion,
latestDraftVersion,
t: i18n.t,
version: optionWithSameID.doc,
}).label
return (
<React.Fragment key={`${optionWithSameID.value}-${index}`}>
{index > 0 ? ', ' : ''}
{label}
</React.Fragment>
)
})}
{')'}
</span>
) : undefined
versionFromComparisonOptions.push({
label: formatPill({
doc: option.doc,
labelOverride: option.labelOverride,
labelSuffix,
}),
value: option.value,
})
}
return (
<DefaultVersionView
canUpdate={docPermissions?.update}
doc={doc}
latestDraftVersion={latestDraftVersion?.id}
latestPublishedVersion={latestPublishedVersion?.id}
modifiedOnly={modifiedOnly}
RenderedDiff={RenderedDiff}
selectedLocales={selectedLocales}
versionID={versionID}
versionFromCreatedAt={versionFrom?.createdAt}
versionFromID={versionFrom?.id}
versionFromOptions={versionFromComparisonOptions}
versionToCreatedAt={versionTo.createdAt}
versionToCreatedAtFormatted={versionToCreatedAtFormatted}
VersionToCreatedAtLabel={formatPill({ doc: versionTo, labelStyle: 'pill' })}
versionToID={versionTo.id}
versionToStatus={versionTo.version?._status}
versionToUseAsTitle={versionTo[collectionConfig.admin?.useAsTitle || 'id']}
/>
)
}

View File

@@ -36,7 +36,7 @@ export const generateVersionViewMetadata: GenerateEditViewMetadata = async ({
metaToUse = {
...(config.admin.meta || {}),
description: t('version:viewingVersion', { documentTitle: doc[useAsTitle], entityLabel }),
description: t('version:viewingVersion', { documentTitle: titleFromData, entityLabel }),
title: `${t('version:version')}${formattedCreatedAt ? ` - ${formattedCreatedAt}` : ''}${titleFromData ? ` - ${titleFromData}` : ''} - ${entityLabel}`,
...(collectionConfig?.admin?.meta || {}),
...(collectionConfig?.admin?.components?.views?.edit?.version?.meta || {}),

View File

@@ -12,29 +12,38 @@ import { SortColumn } from '@payloadcms/ui'
import React from 'react'
import { AutosaveCell } from './cells/AutosaveCell/index.js'
import { CreatedAtCell } from './cells/CreatedAt/index.js'
import { CreatedAtCell, type CreatedAtCellProps } from './cells/CreatedAt/index.js'
import { IDCell } from './cells/ID/index.js'
export const buildVersionColumns = ({
collectionConfig,
CreatedAtCellOverride,
currentlyPublishedVersion,
docID,
docs,
globalConfig,
i18n: { t },
latestDraftVersion,
latestPublishedVersion,
}: {
collectionConfig?: SanitizedCollectionConfig
config: SanitizedConfig
CreatedAtCellOverride?: React.ComponentType<CreatedAtCellProps>
currentlyPublishedVersion?: {
id: number | string
updatedAt: string
}
docID?: number | string
docs: PaginatedDocs<TypeWithVersion<any>>['docs']
globalConfig?: SanitizedGlobalConfig
i18n: I18n
latestDraftVersion?: string
latestPublishedVersion?: string
latestDraftVersion?: {
id: number | string
updatedAt: string
}
}): Column[] => {
const entityConfig = collectionConfig || globalConfig
const CreatedAtCellComponent = CreatedAtCellOverride ?? CreatedAtCell
const columns: Column[] = [
{
accessor: 'updatedAt',
@@ -46,7 +55,7 @@ export const buildVersionColumns = ({
Heading: <SortColumn Label={t('general:updatedAt')} name="updatedAt" />,
renderedCells: docs.map((doc, i) => {
return (
<CreatedAtCell
<CreatedAtCellComponent
collectionSlug={collectionConfig?.slug}
docID={docID}
globalSlug={globalConfig?.slug}
@@ -88,9 +97,9 @@ export const buildVersionColumns = ({
renderedCells: docs.map((doc, i) => {
return (
<AutosaveCell
currentlyPublishedVersion={currentlyPublishedVersion}
key={i}
latestDraftVersion={latestDraftVersion}
latestPublishedVersion={latestPublishedVersion}
rowData={doc}
/>
)

View File

@@ -1,85 +1,49 @@
'use client'
import { Pill, useConfig, useTranslation } from '@payloadcms/ui'
import { Pill, useTranslation } from '@payloadcms/ui'
import React from 'react'
import './index.scss'
import { VersionPillLabel } from '../../../Version/VersionPillLabel/VersionPillLabel.js'
const baseClass = 'autosave-cell'
type AutosaveCellProps = {
latestDraftVersion?: string
latestPublishedVersion?: string
rowData?: {
currentlyPublishedVersion?: {
id: number | string
updatedAt: string
}
latestDraftVersion?: {
id: number | string
updatedAt: string
}
rowData: {
autosave?: boolean
id: number | string
publishedLocale?: string
version: {
_status?: string
_status: string
}
}
}
export const renderPill = (data, latestVersion, currentLabel, previousLabel, pillStyle) => {
return (
<React.Fragment>
{data?.id === latestVersion ? (
<Pill pillStyle={pillStyle} size="small">
{currentLabel}
</Pill>
) : (
<Pill size="small">{previousLabel}</Pill>
)}
</React.Fragment>
)
}
export const AutosaveCell: React.FC<AutosaveCellProps> = ({
currentlyPublishedVersion,
latestDraftVersion,
latestPublishedVersion,
rowData = { autosave: undefined, publishedLocale: undefined, version: undefined },
rowData,
}) => {
const { i18n, t } = useTranslation()
const {
config: { localization },
} = useConfig()
const publishedLocale = rowData?.publishedLocale || undefined
const status = rowData?.version._status
let publishedLocalePill = null
const versionInfo = {
draft: {
currentLabel: t('version:currentDraft'),
latestVersion: latestDraftVersion,
pillStyle: undefined,
previousLabel: t('version:draft'),
},
published: {
currentLabel: t('version:currentPublishedVersion'),
latestVersion: latestPublishedVersion,
pillStyle: 'success',
previousLabel: t('version:previouslyPublished'),
},
}
const { currentLabel, latestVersion, pillStyle, previousLabel } = versionInfo[status] || {}
if (localization && localization?.locales && publishedLocale) {
const localeCode = Array.isArray(publishedLocale) ? publishedLocale[0] : publishedLocale
const locale = localization.locales.find((loc) => loc.code === localeCode)
const formattedLabel = locale?.label?.[i18n?.language] || locale?.label
if (formattedLabel) {
publishedLocalePill = <Pill size="small">{formattedLabel}</Pill>
}
}
const { t } = useTranslation()
return (
<div className={`${baseClass}__items`}>
{rowData?.autosave && <Pill size="small">{t('version:autosave')}</Pill>}
{status && renderPill(rowData, latestVersion, currentLabel, previousLabel, pillStyle)}
{publishedLocalePill}
{rowData?.autosave && <Pill>{t('version:autosave')}</Pill>}
<VersionPillLabel
currentlyPublishedVersion={currentlyPublishedVersion}
disableDate={true}
doc={rowData}
labelFirst={false}
labelStyle="pill"
latestDraftVersion={latestDraftVersion}
/>
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { formatDate } from '@payloadcms/ui/shared'
import { formatAdminURL } from 'payload/shared'
import React from 'react'
type CreatedAtCellProps = {
export type CreatedAtCellProps = {
collectionSlug?: string
docID?: number | string
globalSlug?: string

View File

@@ -1,76 +0,0 @@
import type { Payload, PayloadRequest, Where } from 'payload'
import { logError } from 'payload'
type ReturnType = {
id: string
updatedAt: string
} | null
type Args = {
locale?: string
overrideAccess?: boolean
parentID?: number | string
payload: Payload
req?: PayloadRequest
slug: string
status: 'draft' | 'published'
type: 'collection' | 'global'
}
export async function getLatestVersion(args: Args): Promise<ReturnType> {
const { slug, type = 'collection', locale, overrideAccess, parentID, payload, req, status } = args
const and: Where[] = [
{
'version._status': {
equals: status,
},
},
]
if (type === 'collection' && parentID) {
and.push({
parent: {
equals: parentID,
},
})
}
try {
const sharedOptions = {
depth: 0,
limit: 1,
locale,
overrideAccess,
req,
sort: '-updatedAt',
where: {
and,
},
}
const response =
type === 'collection'
? await payload.findVersions({
collection: slug,
...sharedOptions,
})
: await payload.findGlobalVersions({
slug,
...sharedOptions,
})
if (!response.docs.length) {
return null
}
return {
id: response.docs[0].id,
updatedAt: response.docs[0].updatedAt,
}
} catch (err) {
logError({ err, payload })
return null
}
}

View File

@@ -1,36 +1,40 @@
import { Gutter, ListQueryProvider, SetDocumentStepNav } from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
import { type DocumentViewServerProps, logError, type PaginatedDocs } from 'payload'
import { type DocumentViewServerProps, type PaginatedDocs, type Where } from 'payload'
import { isNumber } from 'payload/shared'
import React from 'react'
import { fetchLatestVersion, fetchVersions } from '../Version/fetchVersions.js'
import { VersionDrawerCreatedAtCell } from '../Version/SelectComparison/VersionDrawer/CreatedAtCell.js'
import { buildVersionColumns } from './buildColumns.js'
import { getLatestVersion } from './getLatestVersion.js'
import { VersionsViewClient } from './index.client.js'
import './index.scss'
export const baseClass = 'versions'
const baseClass = 'versions'
export async function VersionsView(props: DocumentViewServerProps) {
const { initPageResult, searchParams } = props
const {
collectionConfig,
docID: id,
globalConfig,
req,
req: {
i18n,
payload,
payload: { config },
t,
user,
hasPublishedDoc,
initPageResult: {
collectionConfig,
docID: id,
globalConfig,
req,
req: {
i18n,
payload: { config },
t,
user,
},
},
} = initPageResult
searchParams: { limit, page, sort },
versions: { disableGutter = false, useVersionDrawerCreatedAtCell = false } = {},
} = props
const draftsEnabled = (collectionConfig ?? globalConfig)?.versions?.drafts
const collectionSlug = collectionConfig?.slug
const globalSlug = globalConfig?.slug
const { limit, page, sort } = searchParams
const {
localization,
@@ -38,177 +42,110 @@ export async function VersionsView(props: DocumentViewServerProps) {
serverURL,
} = config
let versionsData: PaginatedDocs
let limitToUse = isNumber(limit) ? Number(limit) : undefined
let latestPublishedVersion = null
let latestDraftVersion = null
const whereQuery: {
and: Array<{ parent?: { equals: number | string }; snapshot?: { not_equals: boolean } }>
} & Where = {
and: [],
}
if (localization && draftsEnabled) {
whereQuery.and.push({
snapshot: {
not_equals: true,
},
})
}
if (collectionSlug) {
limitToUse = limitToUse || collectionConfig.admin.pagination.defaultLimit
const whereQuery: {
and: Array<{ parent?: { equals: number | string }; snapshot?: { not_equals: boolean } }>
} = {
and: [
{
parent: {
equals: id,
},
},
],
}
const defaultLimit = collectionSlug ? collectionConfig?.admin?.pagination?.defaultLimit : 10
if (localization && collectionConfig?.versions?.drafts) {
whereQuery.and.push({
snapshot: {
not_equals: true,
},
})
}
const limitToUse = isNumber(limit) ? Number(limit) : defaultLimit
try {
versionsData = await payload.findVersions({
collection: collectionSlug,
depth: 0,
limit: limitToUse,
overrideAccess: false,
page: page ? parseInt(page.toString(), 10) : undefined,
req,
sort: sort as string,
user,
where: whereQuery,
})
if (collectionConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion({
slug: collectionSlug,
type: 'collection',
parentID: id,
payload,
status: 'draft',
})
const publishedDoc = await payload.count({
collection: collectionSlug,
const versionsData: PaginatedDocs = await fetchVersions({
collectionSlug,
depth: 0,
globalSlug,
limit: limitToUse,
overrideAccess: false,
page: page ? parseInt(page.toString(), 10) : undefined,
parentID: id,
req,
sort: sort as string,
user,
where: whereQuery,
})
if (!versionsData) {
return notFound()
}
const [currentlyPublishedVersion, latestDraftVersion] = await Promise.all([
hasPublishedDoc
? fetchLatestVersion({
collectionSlug,
depth: 0,
overrideAccess: true,
globalSlug,
overrideAccess: false,
parentID: id,
req,
where: {
id: {
equals: id,
},
_status: {
equals: 'published',
},
select: {
id: true,
updatedAt: true,
},
})
// If we pass a latestPublishedVersion to buildVersionColumns,
// this will be used to display it as the "current published version".
// However, the latest published version might have been unpublished in the meantime.
// Hence, we should only pass the latest published version if there is a published document.
latestPublishedVersion =
publishedDoc.totalDocs > 0 &&
(await getLatestVersion({
slug: collectionSlug,
type: 'collection',
parentID: id,
payload,
status: 'published',
}))
}
} catch (err) {
logError({ err, payload })
}
}
if (globalSlug) {
limitToUse = limitToUse || 10
const whereQuery =
localization && globalConfig?.versions?.drafts
? {
snapshot: {
not_equals: true,
},
}
: {}
try {
versionsData = await payload.findGlobalVersions({
slug: globalSlug,
depth: 0,
limit: limitToUse,
overrideAccess: false,
page: page ? parseInt(page as string, 10) : undefined,
req,
sort: sort as string,
user,
where: whereQuery,
})
if (globalConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion({
slug: globalSlug,
type: 'global',
payload,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug: globalSlug,
type: 'global',
payload,
status: 'published',
user,
})
}
} catch (err) {
logError({ err, payload })
}
: Promise.resolve(null),
draftsEnabled
? fetchLatestVersion({
collectionSlug,
depth: 0,
globalSlug,
overrideAccess: false,
parentID: id,
req,
select: {
id: true,
updatedAt: true,
},
status: 'draft',
user,
})
: Promise.resolve(null),
])
if (!versionsData) {
return notFound()
}
}
const fetchURL = collectionSlug
? `${serverURL}${apiRoute}/${collectionSlug}/versions`
: globalSlug
? `${serverURL}${apiRoute}/globals/${globalSlug}/versions`
: ''
const publishedNewerThanDraft = latestPublishedVersion?.updatedAt > latestDraftVersion?.updatedAt
if (publishedNewerThanDraft) {
latestDraftVersion = {
id: '',
updatedAt: '',
}
}
: `${serverURL}${apiRoute}/globals/${globalSlug}/versions`
const columns = buildVersionColumns({
collectionConfig,
config,
CreatedAtCellOverride: useVersionDrawerCreatedAtCell ? VersionDrawerCreatedAtCell : undefined,
currentlyPublishedVersion,
docID: id,
docs: versionsData?.docs,
globalConfig,
i18n,
latestDraftVersion: latestDraftVersion?.id,
latestPublishedVersion: latestPublishedVersion?.id,
latestDraftVersion,
})
const pluralLabel = collectionConfig?.labels?.plural
? typeof collectionConfig.labels.plural === 'function'
const pluralLabel =
typeof collectionConfig?.labels?.plural === 'function'
? collectionConfig.labels.plural({ i18n, t })
: collectionConfig.labels.plural
: globalConfig?.label
: (collectionConfig?.labels?.plural ?? globalConfig?.label)
const GutterComponent = disableGutter ? React.Fragment : Gutter
return (
<React.Fragment>
<SetDocumentStepNav
collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug}
collectionSlug={collectionSlug}
globalSlug={globalSlug}
id={id}
pluralLabel={pluralLabel}
useAsTitle={collectionConfig?.admin?.useAsTitle || globalConfig?.slug}
useAsTitle={collectionConfig?.admin?.useAsTitle || globalSlug}
view={i18n.t('version:versions')}
/>
<main className={baseClass}>
<Gutter className={`${baseClass}__wrap`}>
<GutterComponent className={`${baseClass}__wrap`}>
<ListQueryProvider
data={versionsData}
defaultLimit={limitToUse}
@@ -223,7 +160,7 @@ export async function VersionsView(props: DocumentViewServerProps) {
paginationLimits={collectionConfig?.admin?.pagination?.limits}
/>
</ListQueryProvider>
</Gutter>
</GutterComponent>
</main>
</React.Fragment>
)

View File

@@ -29,6 +29,8 @@ export type VersionField = {
/**
* Taken from react-diff-viewer-continued
*
* @deprecated remove in 4.0 - react-diff-viewer-continued is no longer a dependency
*/
export declare enum DiffMethod {
CHARS = 'diffChars',
@@ -44,10 +46,13 @@ export declare enum DiffMethod {
export type FieldDiffClientProps<TClientField extends ClientFieldWithOptionalType = ClientField> = {
baseVersionField: BaseVersionField
/**
* Field value from the version being compared
* Field value from the version being compared from
*/
comparisonValue: unknown
diffMethod: DiffMethod
comparisonValue: unknown // TODO: change to valueFrom in 4.0
/**
* @deprecated remove in 4.0. react-diff-viewer-continued is no longer a dependency
*/
diffMethod: any
field: TClientField
fieldPermissions:
| {
@@ -58,11 +63,13 @@ export type FieldDiffClientProps<TClientField extends ClientFieldWithOptionalTyp
* If this field is localized, this will be the locale of the field
*/
locale?: string
nestingLevel?: number
parentIsLocalized: boolean
/**
* Field value from the current version
* Field value from the version being compared to
*
*/
versionValue: unknown
versionValue: unknown // TODO: change to valueTo in 4.0
}
export type FieldDiffServerProps<

View File

@@ -22,9 +22,10 @@ export type ServerFunctionClientArgs = {
export type ServerFunctionClient = (args: ServerFunctionClientArgs) => Promise<unknown> | unknown
export type ServerFunction = (
args: DefaultServerFunctionArgs & ServerFunctionClientArgs['args'],
) => Promise<unknown> | unknown
export type ServerFunction<
TArgs extends object = Record<string, unknown>,
TReturnType = Promise<unknown> | unknown,
> = (args: DefaultServerFunctionArgs & TArgs) => TReturnType
export type ServerFunctionConfig = {
fn: ServerFunction

View File

@@ -625,6 +625,7 @@ export type {
DocumentViewServerProps,
DocumentViewServerPropsOnly,
EditViewProps,
RenderDocumentVersionsProperties,
} from './views/document.js'
export type {

View File

@@ -9,11 +9,27 @@ export type EditViewProps = {
readonly collectionSlug?: string
readonly globalSlug?: string
}
/**
* Properties specific to the versions view
*/
export type RenderDocumentVersionsProperties = {
/**
* @default false
*/
disableGutter?: boolean
/**
* Use createdAt cell that appends params to the url on version selection instead of redirecting user
* @default false
*/
useVersionDrawerCreatedAtCell?: boolean
}
export type DocumentViewServerPropsOnly = {
readonly doc: Data
readonly initPageResult: InitPageResult
readonly routeSegments: string[]
doc: Data
hasPublishedDoc: boolean
initPageResult: InitPageResult
routeSegments: string[]
versions?: RenderDocumentVersionsProperties
} & ServerProps
export type DocumentViewServerProps = DocumentViewClientProps & DocumentViewServerPropsOnly

View File

@@ -1,5 +1,4 @@
@import '~@payloadcms/ui/scss';
@import '../../colors.scss';
@layer payload-default {
.lexical-diff {

View File

@@ -48,7 +48,7 @@ export const ListItemDiffHTMLConverterAsync: HTMLConvertersAsync<SerializedListI
</li>
)
const html = ReactDOMServer.renderToString(JSX)
const html = ReactDOMServer.renderToStaticMarkup(JSX)
// Add style="list-style-type: none;${providedCSSString}" to html
const styleIndex = html.indexOf('class="list-item-checkbox')

View File

@@ -1,79 +1,73 @@
@import '~@payloadcms/ui/scss';
@import '../../colors.scss';
@layer payload-default {
.lexical-diff__diff-container {
.lexical-relationship-diff {
@extend %body;
@include shadow-sm;
min-width: calc(var(--base) * 8);
max-width: fit-content;
.lexical-diff .lexical-relationship-diff {
@extend %body;
@include shadow-sm;
min-width: calc(var(--base) * 8);
max-width: fit-content;
display: flex;
align-items: center;
background-color: var(--theme-input-bg);
border-radius: $style-radius-s;
border: 1px solid var(--theme-elevation-100);
position: relative;
font-family: var(--font-body);
margin-block: base(0.5);
max-height: calc(var(--base) * 4);
padding: base(0.6);
display: flex;
align-items: center;
background-color: var(--theme-input-bg);
border-radius: $style-radius-s;
border: 1px solid var(--theme-elevation-100);
position: relative;
font-family: var(--font-body);
margin-block: base(0.5);
max-height: calc(var(--base) * 4);
padding: base(0.6);
&[data-match-type='create'] {
border-color: var(--diff-create-pill-border);
&[data-match-type='create'] {
border-color: var(--diff-create-pill-border);
color: var(--diff-create-parent-color);
.lexical-relationship-diff__collectionLabel {
color: var(--diff-create-link-color);
}
.lexical-relationship-diff__title * {
color: var(--diff-create-parent-color);
}
}
.lexical-relationship-diff__collectionLabel {
color: var(--diff-create-link-color);
}
&[data-match-type='delete'] {
border-color: var(--diff-delete-pill-border);
color: var(--diff-delete-parent-color);
text-decoration-line: none;
background-color: var(--diff-delete-pill-bg);
[data-match-type='create'] {
color: var(--diff-create-parent-color);
}
.lexical-relationship-diff__collectionLabel {
color: var(--diff-delete-link-color);
}
&[data-match-type='delete'] {
border-color: var(--diff-delete-pill-border);
color: var(--diff-delete-parent-color);
* {
text-decoration-line: none;
background-color: var(--diff-delete-pill-bg);
.lexical-relationship-diff__collectionLabel {
color: var(--diff-delete-link-color);
}
[data-match-type='delete'] {
text-decoration-line: none;
}
* {
color: var(--diff-delete-parent-color);
}
color: var(--diff-delete-parent-color);
}
}
&__card {
display: flex;
flex-direction: column;
width: 100%;
flex-grow: 1;
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: space-between;
}
&__card {
display: flex;
flex-direction: column;
width: 100%;
flex-grow: 1;
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: space-between;
}
&__title {
display: flex;
flex-direction: row;
font-weight: 600;
}
&__title {
display: flex;
flex-direction: row;
font-weight: 600;
}
&__collectionLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__collectionLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}

View File

@@ -4,6 +4,8 @@ import { getTranslation, type I18nClient } from '@payloadcms/translations'
import './index.scss'
import { formatAdminURL } from 'payload/shared'
import type { HTMLConvertersAsync } from '../../../../features/converters/lexicalToHtml/async/types.js'
import type { SerializedRelationshipNode } from '../../../../nodeTypes.js'
@@ -17,13 +19,15 @@ export const RelationshipDiffHTMLConverterAsync: (args: {
relationship: async ({ node, populate, providedCSSString }) => {
let data: (Record<string, any> & TypeWithID) | undefined = undefined
const id = typeof node.value === 'object' ? node.value.id : node.value
// If there's no valid upload data, populate return an empty string
if (typeof node.value !== 'object') {
if (!populate) {
return ''
}
data = await populate<FileData & TypeWithID>({
id: node.value,
id,
collectionSlug: node.relationTo,
})
} else {
@@ -38,7 +42,7 @@ export const RelationshipDiffHTMLConverterAsync: (args: {
<div
className={`${baseClass}${providedCSSString}`}
data-enable-match="true"
data-id={node.value}
data-id={id}
data-slug={node.relationTo}
>
<div className={`${baseClass}__card`}>
@@ -56,7 +60,11 @@ export const RelationshipDiffHTMLConverterAsync: (args: {
<a
className={`${baseClass}__link`}
data-enable-match="false"
href={`/${relatedCollection.slug}/${data.id}`}
href={formatAdminURL({
adminRoute: req.payload.config.routes.admin,
path: `/collections/${relatedCollection?.slug}/${data.id}`,
serverURL: req.payload.config.serverURL,
})}
rel="noopener noreferrer"
target="_blank"
>
@@ -64,14 +72,14 @@ export const RelationshipDiffHTMLConverterAsync: (args: {
</a>
</strong>
) : (
<strong>{node.value as string}</strong>
<strong>{id as string}</strong>
)}
</div>
</div>
)
// Render to HTML
const html = ReactDOMServer.renderToString(JSX)
const html = ReactDOMServer.renderToStaticMarkup(JSX)
return html
},

View File

@@ -1,42 +1,39 @@
@import '~@payloadcms/ui/scss';
@import '../../colors.scss';
@layer payload-default {
.lexical-diff__diff-container {
.lexical-unknown-diff {
@extend %body;
@include shadow-sm;
max-width: fit-content;
display: flex;
align-items: center;
background: var(--theme-input-bg);
border-radius: $style-radius-s;
border: 1px solid var(--theme-elevation-100);
position: relative;
font-family: var(--font-body);
margin-block: base(0.5);
max-height: calc(var(--base) * 4);
padding: base(0.25);
.lexical-diff .lexical-unknown-diff {
@extend %body;
@include shadow-sm;
max-width: fit-content;
display: flex;
align-items: center;
background: var(--theme-input-bg);
border-radius: $style-radius-s;
border: 1px solid var(--theme-elevation-100);
position: relative;
font-family: var(--font-body);
margin-block: base(0.5);
max-height: calc(var(--base) * 4);
padding: base(0.25);
&__specifier {
font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace;
}
&__specifier {
font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace;
}
&[data-match-type='create'] {
border-color: var(--diff-create-pill-border);
color: var(--diff-create-parent-color);
}
&[data-match-type='create'] {
border-color: var(--diff-create-pill-border);
color: var(--diff-create-parent-color);
}
&[data-match-type='delete'] {
border-color: var(--diff-delete-pill-border);
color: var(--diff-delete-parent-color);
&[data-match-type='delete'] {
border-color: var(--diff-delete-pill-border);
color: var(--diff-delete-parent-color);
text-decoration-line: none;
background-color: var(--diff-delete-pill-bg);
* {
text-decoration-line: none;
background-color: var(--diff-delete-pill-bg);
* {
text-decoration-line: none;
color: var(--diff-delete-parent-color);
}
color: var(--diff-delete-parent-color);
}
}
}

View File

@@ -54,7 +54,7 @@ export const UnknownDiffHTMLConverterAsync: (args: {
)
// Render to HTML
const html = ReactDOMServer.renderToString(JSX)
const html = ReactDOMServer.renderToStaticMarkup(JSX)
return html
},

View File

@@ -1,8 +1,7 @@
@import '~@payloadcms/ui/scss';
@import '../../colors.scss';
@layer payload-default {
.lexical-diff__diff-container {
.lexical-diff {
.lexical-upload-diff {
@extend %body;
@include shadow-sm;

View File

@@ -72,7 +72,7 @@ export const UploadDiffHTMLConverterAsync: (args: {
<File />
)}
</div>
<div className={`${baseClass}__info`}>
<div className={`${baseClass}__info`} data-enable-match="false">
<strong>{uploadDoc?.filename}</strong>
<div className={`${baseClass}__meta`}>
{formatFilesize(uploadDoc?.filesize)}
@@ -95,7 +95,7 @@ export const UploadDiffHTMLConverterAsync: (args: {
)
// Render to HTML
const html = ReactDOMServer.renderToString(JSX)
const html = ReactDOMServer.renderToStaticMarkup(JSX)
return html
},

View File

@@ -1,90 +0,0 @@
@import '~@payloadcms/ui/scss';
@import '../colors.scss';
@layer payload-default {
.lexical-diff__diff-container {
font-family: var(--font-serif);
font-size: base(0.8);
letter-spacing: 0.02em;
// Apply background color to parents that have children with diffs
p,
li,
h1,
h2,
h3,
h4,
h5,
blockquote,
h6 {
&:has([data-match-type='create']) {
background-color: var(--diff-create-parent-bg);
color: var(--diff-create-parent-color);
}
&:has([data-match-type='delete']) {
background-color: var(--diff-delete-parent-bg);
color: var(--diff-delete-parent-color);
}
}
li::marker {
color: var(--theme-text);
}
[data-match-type='delete'] {
color: var(--diff-delete-pill-color);
text-decoration-color: var(--diff-delete-pill-color);
text-decoration-line: line-through;
background-color: var(--diff-delete-pill-bg);
border-radius: 4px;
text-decoration-thickness: 1px;
}
a[data-match-type='delete'] {
color: var(--diff-delete-link-color);
}
a[data-match-type='create']:not(img) {
// :not(img) required to increase specificity
color: var(--diff-create-link-color);
}
[data-match-type='create']:not(img) {
background-color: var(--diff-create-pill-bg);
color: var(--diff-create-pill-color);
border-radius: 4px;
}
.html-diff {
&-create-inline-wrapper,
&-delete-inline-wrapper {
display: inline-flex;
}
&-create-block-wrapper,
&-delete-block-wrapper {
display: flex;
}
&-create-inline-wrapper,
&-delete-inline-wrapper,
&-create-block-wrapper,
&-delete-block-wrapper {
position: relative;
align-items: center;
flex-direction: row;
&::after {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100%;
content: '';
}
}
}
}
}

View File

@@ -1,30 +1,37 @@
@import '~@payloadcms/ui/scss';
@import './colors.scss';
@layer payload-default {
.lexical-diff {
&__diff-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
.lexical-diff .field-diff-content {
.html-diff {
font-family: var(--font-serif);
font-size: base(0.8);
letter-spacing: 0.02em;
}
blockquote {
font-size: base(0.8);
margin-block: base(0.8);
margin-inline: base(0.2);
border-inline-start-color: var(--theme-elevation-150);
border-inline-start-width: base(0.2);
border-inline-start-style: solid;
margin-inline: 0;
padding-inline-start: base(0.6);
padding-block: base(0.2);
position: relative; // Required for absolute positioning of ::after
&:has([data-match-type='create']) {
border-inline-start-color: var(--theme-success-150);
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: 0;
width: base(0.2);
background-color: var(--theme-elevation-150);
}
&:has([data-match-type='delete']) {
border-inline-start-color: var(--theme-error-150);
&:has([data-match-type='create'])::after {
background-color: var(--theme-success-150);
}
&:has([data-match-type='delete'])::after {
background-color: var(--theme-error-150);
}
}
@@ -35,45 +42,45 @@
h1 {
padding: base(0.7) 0px base(0.55);
line-height: base(1.2);
line-height: base(1.5);
font-weight: 600;
font-size: base(1.4);
font-family: var(--font-body);
}
h2 {
padding: base(0.7) 0px base(0.5);
line-height: base(1);
line-height: base(1.4);
font-weight: 600;
font-size: base(1.25);
font-family: var(--font-body);
}
h3 {
padding: base(0.65) 0px base(0.45);
line-height: base(0.9);
padding: base(0.6) 0px base(0.45);
line-height: base(1.4);
font-weight: 600;
font-size: base(1.1);
font-family: var(--font-body);
}
h4 {
padding: base(0.65) 0px base(0.4);
line-height: base(0.7);
padding: base(0.4) 0px base(0.35);
line-height: base(1.5);
font-weight: 600;
font-size: base(1);
font-size: base(1.05);
font-family: var(--font-body);
}
h5 {
padding: base(0.65) 0px base(0.35);
line-height: base(0.5);
padding: base(0.3) 0px base(0.3);
line-height: base(1.4);
font-weight: 600;
font-size: base(0.9);
font-family: var(--font-body);
}
h6 {
padding: base(0.65) 0px base(0.35);
padding: base(0.55) 0px base(0.25);
line-height: base(0.5);
font-weight: 600;
font-size: base(0.8);
font-size: base(0.75);
font-family: var(--font-body);
}

View File

@@ -1,14 +1,13 @@
import type { SerializedEditorState } from 'lexical'
import type { RichTextFieldDiffServerComponent } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { FieldDiffLabel } from '@payloadcms/ui/rsc'
import React from 'react'
import { FieldDiffContainer, getHTMLDiffComponents } from '@payloadcms/ui/rsc'
import './htmlDiff/index.scss'
import './index.scss'
import '../bundled.css'
import React from 'react'
import type { HTMLConvertersFunctionAsync } from '../../features/converters/lexicalToHtml/async/types.js'
import { convertLexicalToHTMLAsync } from '../../features/converters/lexicalToHtml/async/index.js'
@@ -18,11 +17,18 @@ import { ListItemDiffHTMLConverterAsync } from './converters/listitem/index.js'
import { RelationshipDiffHTMLConverterAsync } from './converters/relationship/index.js'
import { UnknownDiffHTMLConverterAsync } from './converters/unknown/index.js'
import { UploadDiffHTMLConverterAsync } from './converters/upload/index.js'
import { HtmlDiff } from './htmlDiff/index.js'
const baseClass = 'lexical-diff'
export const LexicalDiffComponent: RichTextFieldDiffServerComponent = async (args) => {
const { comparisonValue, field, i18n, locale, versionValue } = args
const {
comparisonValue: valueFrom,
field,
i18n,
locale,
nestingLevel,
versionValue: valueTo,
} = args
const converters: HTMLConvertersFunctionAsync = ({ defaultConverters }) => ({
...defaultConverters,
@@ -38,38 +44,37 @@ export const LexicalDiffComponent: RichTextFieldDiffServerComponent = async (arg
depth: 1,
req: args.req,
})
const comparisonHTML = await convertLexicalToHTMLAsync({
const fromHTML = await convertLexicalToHTMLAsync({
converters,
data: comparisonValue as SerializedEditorState,
data: valueFrom as SerializedEditorState,
disableContainer: true,
populate: payloadPopulateFn,
})
const versionHTML = await convertLexicalToHTMLAsync({
const toHTML = await convertLexicalToHTMLAsync({
converters,
data: versionValue as SerializedEditorState,
data: valueTo as SerializedEditorState,
disableContainer: true,
populate: payloadPopulateFn,
})
const diffHTML = new HtmlDiff(comparisonHTML, versionHTML)
const [oldHTML, newHTML] = diffHTML.getSideBySideContents()
const { From, To } = getHTMLDiffComponents({
// Ensure empty paragraph is displayed for empty rich text fields - otherwise, toHTML may be displayed in the wrong column
fromHTML: fromHTML?.length ? fromHTML : '<p></p>',
toHTML: toHTML?.length ? toHTML : '<p></p>',
})
return (
<div className={baseClass}>
<FieldDiffLabel>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{'label' in field &&
typeof field.label !== 'function' &&
getTranslation(field.label || '', i18n)}
</FieldDiffLabel>
<div className={`${baseClass}__diff-container`}>
{oldHTML && (
<div className={`${baseClass}__diff-old`} dangerouslySetInnerHTML={{ __html: oldHTML }} />
)}
{newHTML && (
<div className={`${baseClass}__diff-new`} dangerouslySetInnerHTML={{ __html: newHTML }} />
)}
</div>
</div>
<FieldDiffContainer
className={baseClass}
From={From}
i18n={i18n}
label={{
label: field.label,
locale,
}}
nestingLevel={nestingLevel}
To={To}
/>
)
}

View File

@@ -236,6 +236,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:livePreview',
'general:loading',
'general:locale',
'general:locales',
'general:menu',
'general:moreOptions',
'general:move',
@@ -409,15 +410,20 @@ export const clientTranslationKeys = createClientTranslationKeys([
'version:autosave',
'version:autosavedSuccessfully',
'version:autosavedVersion',
'version:versionAgo',
'version:moreVersions',
'version:changed',
'version:changedFieldsCount',
'version:confirmRevertToSaved',
'version:compareVersion',
'version:compareVersions',
'version:comparingAgainst',
'version:currentlyViewing',
'version:confirmPublish',
'version:confirmUnpublish',
'version:confirmVersionRestoration',
'version:currentDraft',
'version:currentPublishedVersion',
'version:currentlyPublished',
'version:draft',
'version:draftSavedSuccessfully',
'version:lastSavedAgo',
@@ -427,6 +433,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'version:noRowsSelected',
'version:preview',
'version:previouslyPublished',
'version:previousVersion',
'version:problemRestoringVersion',
'version:publish',
'version:publishAllLocales',
@@ -446,6 +453,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'version:selectLocales',
'version:selectVersionToCompare',
'version:showLocales',
'version:specificVersion',
'version:status',
'version:type',
'version:unpublish',

View File

@@ -18,11 +18,12 @@ export const importDateFNSLocale = async (locale: string): Promise<Locale> => {
break
case 'bn-BD':
result = (await import('date-fns/locale/bn')).bn
break
case 'bn-IN':
result = (await import('date-fns/locale/bn')).bn
break
break
case 'ca':
result = (await import('date-fns/locale/ca')).ca

View File

@@ -489,22 +489,28 @@ export const arTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} قام بتغيير الحقل',
changedFieldsCount_other: '{{count}} حقول تم تغييرها',
compareVersion: 'مقارنة النّسخة مع:',
compareVersions: 'قارن الإصدارات',
comparingAgainst: 'مقارنة مع',
confirmPublish: 'تأكيد النّشر',
confirmRevertToSaved: 'تأكيد الرّجوع للنسخة المنشورة',
confirmUnpublish: 'تأكيد إلغاء النّشر',
confirmVersionRestoration: 'تأكيد إستعادة النّسخة',
currentDocumentStatus: 'المستند {{docStatus}} الحالي',
currentDraft: 'المسودة الحالية',
currentlyPublished: 'نشر حاليا',
currentlyViewing: 'تمت المشاهدة حاليا',
currentPublishedVersion: 'النسخة المنشورة الحالية',
draft: 'مسودّة',
draftSavedSuccessfully: 'تمّ حفظ المسودّة بنجاح.',
lastSavedAgo: 'تم الحفظ آخر مرة قبل {{distance}}',
modifiedOnly: 'تم التعديل فقط',
moreVersions: 'المزيد من الإصدارات...',
noFurtherVersionsFound: 'لم يتمّ العثور على نسخات أخرى',
noRowsFound: 'لم يتمّ العثور على {{label}}',
noRowsSelected: 'لم يتم اختيار {{label}}',
preview: 'معاينة',
previouslyPublished: 'نشر سابقا',
previousVersion: 'النسخة السابقة',
problemRestoringVersion: 'حدث خطأ في استعادة هذه النّسخة',
publish: 'نشر',
publishAllLocales: 'نشر جميع المواقع',
@@ -525,10 +531,12 @@ export const arTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'حدّد نسخة للمقارنة',
showingVersionsFor: 'يتمّ عرض النًّسخ ل:',
showLocales: 'اظهر اللّغات:',
specificVersion: 'الإصدار المحدد',
status: 'الحالة',
unpublish: 'الغاء النّشر',
unpublishing: 'يتمّ الغاء النّشر...',
version: 'النّسخة',
versionAgo: 'منذ {{distance}}',
versionCount_many: 'تمّ العثور على {{count}} نُسخ',
versionCount_none: 'لم يتمّ العثور على أيّ من النّسخ',
versionCount_one: 'تمّ العثور على {{count}} من النّسخ',

View File

@@ -500,22 +500,28 @@ export const azTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} sahə dəyişdi',
changedFieldsCount_other: '{{count}} dəyişdirilmiş sahələr',
compareVersion: 'Versiyanı müqayisə et:',
compareVersions: 'Versiyaları Müqayisə Edin',
comparingAgainst: 'Müqayisə etmək',
confirmPublish: 'Dərci təsdiq edin',
confirmRevertToSaved: 'Yadda saxlanana qayıtmağı təsdiq edin',
confirmUnpublish: 'Dərcdən çıxartmağı təsdiq edin',
confirmVersionRestoration: 'Versiyanın bərpasını təsdiq edin',
currentDocumentStatus: 'Cari {{docStatus}} sənədi',
currentDraft: 'Hazırki Layihə',
currentlyPublished: 'Hazırda Nəşr Olunmuş',
currentlyViewing: 'Hazırda baxılır',
currentPublishedVersion: 'Hazırki Nəşr Versiyası',
draft: 'Qaralama',
draftSavedSuccessfully: 'Qaralama uğurla yadda saxlandı.',
lastSavedAgo: '{{distance}} əvvəl son yadda saxlanıldı',
modifiedOnly: 'Yalnızca dəyişdirilmişdir',
moreVersions: 'Daha çox versiyalar...',
noFurtherVersionsFound: 'Başqa versiyalar tapılmadı',
noRowsFound: 'Heç bir {{label}} tapılmadı',
noRowsSelected: 'Heç bir {{label}} seçilməyib',
preview: 'Öncədən baxış',
previouslyPublished: 'Daha əvvəl nəşr olunmuş',
previousVersion: 'Əvvəlki Versiya',
problemRestoringVersion: 'Bu versiyanın bərpasında problem yaşandı',
publish: 'Dərc et',
publishAllLocales: 'Bütün lokalizasiyaları dərc edin',
@@ -536,10 +542,12 @@ export const azTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Müqayisə üçün bir versiya seçin',
showingVersionsFor: 'Göstərilən versiyalar üçün:',
showLocales: 'Lokalları göstər:',
specificVersion: 'Xüsusi Versiya',
status: 'Status',
unpublish: 'Dərcdən çıxart',
unpublishing: 'Dərcdən çıxarılır...',
version: 'Versiya',
versionAgo: '{{distance}} əvvəl',
versionCount_many: '{{count}} versiya tapıldı',
versionCount_none: 'Versiya tapılmadı',
versionCount_one: '{{count}} versiya tapıldı',

View File

@@ -499,22 +499,28 @@ export const bgTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} променено поле',
changedFieldsCount_other: '{{count}} променени полета',
compareVersion: 'Сравни версия с:',
compareVersions: 'Сравняване на версии',
comparingAgainst: 'Сравнение с',
confirmPublish: 'Потвърди публикуване',
confirmRevertToSaved: 'Потвърди възстановяване до запазен',
confirmUnpublish: 'Потвърди скриване',
confirmVersionRestoration: 'Потвърди възстановяване на версия',
currentDocumentStatus: 'Сегашен статус на документа: {{docStatus}}',
currentDraft: 'Текущ проект',
currentlyPublished: 'В момента публикуван',
currentlyViewing: 'В момента преглеждате',
currentPublishedVersion: 'Текуща публикувана версия',
draft: 'Чернова',
draftSavedSuccessfully: 'Чернова запазена успешно.',
lastSavedAgo: 'последно запазено преди {{distance}}',
modifiedOnly: 'Само променени',
moreVersions: 'Още версии...',
noFurtherVersionsFound: 'Не са открити повече версии',
noRowsFound: 'Не е открит {{label}}',
noRowsSelected: 'Не е избран {{label}}',
preview: 'Предварителен преглед',
previouslyPublished: 'Предишно публикувано',
previousVersion: 'Предишна версия',
problemRestoringVersion: 'Имаше проблем при възстановяването на тази версия',
publish: 'Публикувай',
publishAllLocales: 'Публикувайте всички локали',
@@ -535,10 +541,12 @@ export const bgTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Избери версия за сравняване',
showingVersionsFor: 'Показване на версии за:',
showLocales: 'Покажи преводи:',
specificVersion: 'Специфична версия',
status: 'Статус',
unpublish: 'Скрий',
unpublishing: 'Скриване...',
version: 'Версия',
versionAgo: 'преди {{distance}}',
versionCount_many: '{{count}} открити версии',
versionCount_none: 'Няма открити версии',
versionCount_one: '{{count}} открита версия',

View File

@@ -500,22 +500,28 @@ export const bnBdTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} পরিবর্তিত ক্ষেত্র',
changedFieldsCount_other: '{{count}} পরিবর্তিত ক্ষেত্রগুলি',
compareVersion: 'সংস্করণের সাথে তুলনা করুন:',
compareVersions: 'সংস্করণ তুলনা করুন',
comparingAgainst: 'তুলনা করা যাচ্ছে',
confirmPublish: 'প্রকাশ নিশ্চিত করুন',
confirmRevertToSaved: 'সংরক্ষিত অবস্থায় ফিরে যাওয়া নিশ্চিত করুন',
confirmUnpublish: 'আনপাবলিশ নিশ্চিত করুন',
confirmVersionRestoration: 'সংস্করণ পুনরুদ্ধার নিশ্চিত করুন',
currentDocumentStatus: 'বর্তমান {{docStatus}} ডকুমেন্ট',
currentDraft: 'বর্তমান খসড়া',
currentlyPublished: 'বর্তমানে প্রকাশিত',
currentlyViewing: 'বর্তমানে দেখছেন',
currentPublishedVersion: 'বর্তমান প্রকাশিত সংস্করণ',
draft: 'খসড়া',
draftSavedSuccessfully: 'খসড়া সফলভাবে সংরক্ষিত হয়েছে।',
lastSavedAgo: 'সর্বশেষ সংরক্ষণ করা হয়েছে {{distance}} আগে',
modifiedOnly: 'শুধুমাত্র পরিবর্তিত',
moreVersions: 'আরও সংস্করণ...',
noFurtherVersionsFound: 'আর কোনো সংস্করণ পাওয়া যায়নি',
noRowsFound: 'কোনো {{label}} পাওয়া যায়নি',
noRowsSelected: 'কোনো {{label}} নির্বাচিত হয়নি',
preview: 'প্রাকদর্শন',
previouslyPublished: 'পূর্বে প্রকাশিত',
previousVersion: 'পূর্ববর্তী সংস্করণ',
problemRestoringVersion: 'এই সংস্করণ পুনরুদ্ধারে সমস্যা হয়েছে',
publish: 'প্রকাশ করুন',
publishAllLocales: 'সমস্ত লোকেল প্রকাশ করুন',
@@ -536,10 +542,12 @@ export const bnBdTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'তুলনার জন্য একটি সংস্করণ নির্বাচন করুন',
showingVersionsFor: 'এর জন্য সংস্করণগুলি দেখানো হচ্ছে:',
showLocales: 'লোকেল দেখান:',
specificVersion: 'নির্দিষ্ট সংস্করণ',
status: 'স্থিতি',
unpublish: 'প্রকাশ বাতিল করুন',
unpublishing: 'প্রকাশ বাতিল করা হচ্ছে...',
version: 'সংস্করণ',
versionAgo: '{{distance}} পূর্বে',
versionCount_many: '{{count}}টি সংস্করণ পাওয়া গেছে',
versionCount_none: 'কোনো সংস্করণ পাওয়া যায়নি',
versionCount_one: '{{count}}টি সংস্করণ পাওয়া গেছে',

View File

@@ -500,22 +500,28 @@ export const bnInTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} পরিবর্তিত ক্ষেত্র',
changedFieldsCount_other: '{{count}} পরিবর্তিত ক্ষেত্রগুলি',
compareVersion: 'সংস্করণের সাথে তুলনা করুন:',
compareVersions: 'সংস্করণগুলি তুলনা করুন',
comparingAgainst: 'তুলনা করে',
confirmPublish: 'প্রকাশ নিশ্চিত করুন',
confirmRevertToSaved: 'সংরক্ষিত অবস্থায় ফিরে যাওয়া নিশ্চিত করুন',
confirmUnpublish: 'আনপাবলিশ নিশ্চিত করুন',
confirmVersionRestoration: 'সংস্করণ পুনরুদ্ধার নিশ্চিত করুন',
currentDocumentStatus: 'বর্তমান {{docStatus}} ডকুমেন্ট',
currentDraft: 'বর্তমান খসড়া',
currentlyPublished: 'বর্তমানে প্রকাশিত',
currentlyViewing: 'বর্তমানে দেখছেন',
currentPublishedVersion: 'বর্তমান প্রকাশিত সংস্করণ',
draft: 'খসড়া',
draftSavedSuccessfully: 'খসড়া সফলভাবে সংরক্ষিত হয়েছে।',
lastSavedAgo: 'সর্বশেষ সংরক্ষণ করা হয়েছে {{distance}} আগে',
modifiedOnly: 'শুধুমাত্র পরিবর্তিত',
moreVersions: 'আরও সংস্করণ...',
noFurtherVersionsFound: 'আর কোনো সংস্করণ পাওয়া যায়নি',
noRowsFound: 'কোনো {{label}} পাওয়া যায়নি',
noRowsSelected: 'কোনো {{label}} নির্বাচিত হয়নি',
preview: 'প্রাকদর্শন',
previouslyPublished: 'পূর্বে প্রকাশিত',
previousVersion: 'পূর্ববর্তী সংস্করণ',
problemRestoringVersion: 'এই সংস্করণ পুনরুদ্ধারে সমস্যা হয়েছে',
publish: 'প্রকাশ করুন',
publishAllLocales: 'সমস্ত লোকেল প্রকাশ করুন',
@@ -536,10 +542,12 @@ export const bnInTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'তুলনার জন্য একটি সংস্করণ নির্বাচন করুন',
showingVersionsFor: 'এর জন্য সংস্করণগুলি দেখানো হচ্ছে:',
showLocales: 'লোকেল দেখান:',
specificVersion: 'নির্দিষ্ট সংস্করণ',
status: 'স্থিতি',
unpublish: 'প্রকাশ বাতিল করুন',
unpublishing: 'প্রকাশ বাতিল করা হচ্ছে...',
version: 'সংস্করণ',
versionAgo: '{{distance}} পূর্বে',
versionCount_many: '{{count}}টি সংস্করণ পাওয়া গেছে',
versionCount_none: 'কোনো সংস্করণ পাওয়া যায়নি',
versionCount_one: '{{count}}টি সংস্করণ পাওয়া গেছে',

View File

@@ -502,22 +502,28 @@ export const caTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} camp canviat',
changedFieldsCount_other: '{{count}} camps modificats',
compareVersion: 'Comparar versió amb:',
compareVersions: 'Compara Versions',
comparingAgainst: 'Comparant amb',
confirmPublish: 'Confirmar publicació',
confirmRevertToSaved: 'Confirmar revertir a desat',
confirmUnpublish: 'Confirmar despublicació',
confirmVersionRestoration: 'Confirmar restauració de versió',
currentDocumentStatus: 'Estat actual del document {{docStatus}}',
currentDraft: 'Borrador actual',
currentlyPublished: 'Actualment publicat',
currentlyViewing: 'Actualment veient',
currentPublishedVersion: 'Versió publicada actual',
draft: 'Borrador',
draftSavedSuccessfully: 'Borrador desat amb èxit.',
lastSavedAgo: 'Últim desament fa {{distance}}',
modifiedOnly: 'Només modificat',
moreVersions: 'Més versions...',
noFurtherVersionsFound: "No s'han trobat més versions",
noRowsFound: "No s'han trobat {{label}}",
noRowsSelected: "No s'han seleccionat {{label}}",
preview: 'Vista prèvia',
previouslyPublished: 'Publicat anteriorment',
previousVersion: 'Versió anterior',
problemRestoringVersion: 'Hi ha hagut un problema en restaurar aquesta versió',
publish: 'Publicar',
publishAllLocales: 'Publica totes les configuracions regionals',
@@ -538,10 +544,12 @@ export const caTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Selecciona una versió per comparar',
showingVersionsFor: 'Mostrant versions per a:',
showLocales: 'Mostrar idiomes:',
specificVersion: 'Versió Específica',
status: 'Estat',
unpublish: 'Despublicar',
unpublishing: 'Despublicant...',
version: 'Versió',
versionAgo: 'fa {{distance}}',
versionCount_many: '{{count}} versions trobades',
versionCount_none: "No s'han trobat versions",
versionCount_one: '{{count}} versió trobada',

View File

@@ -496,22 +496,28 @@ export const csTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} změněné pole',
changedFieldsCount_other: '{{count}} změněná pole',
compareVersion: 'Porovnat verzi s:',
compareVersions: 'Porovnat verze',
comparingAgainst: 'Porovnání s',
confirmPublish: 'Potvrďte publikování',
confirmRevertToSaved: 'Potvrdit vrácení k uloženému',
confirmUnpublish: 'Potvrdit zrušení publikování',
confirmVersionRestoration: 'Potvrdit obnovení verze',
currentDocumentStatus: 'Současný {{docStatus}} dokument',
currentDraft: 'Současný koncept',
currentlyPublished: 'Aktuálně publikováno',
currentlyViewing: 'Aktuálně prohlížíte',
currentPublishedVersion: 'Aktuálně publikovaná verze',
draft: 'Koncept',
draftSavedSuccessfully: 'Koncept úspěšně uložen.',
lastSavedAgo: 'Naposledy uloženo před {{distance}}',
modifiedOnly: 'Pouze upraveno',
moreVersions: 'Více verzí...',
noFurtherVersionsFound: 'Nenalezeny další verze',
noRowsFound: 'Nenalezen {{label}}',
noRowsSelected: 'Nebyl vybrán žádný {{label}}',
preview: 'Náhled',
previouslyPublished: 'Dříve publikováno',
previousVersion: 'Předchozí verze',
problemRestoringVersion: 'Při obnovování této verze došlo k problému',
publish: 'Publikovat',
publishAllLocales: 'Publikujte všechny lokalizace',
@@ -532,10 +538,12 @@ export const csTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Vyberte verzi pro porovnání',
showingVersionsFor: 'Zobrazují se verze pro:',
showLocales: 'Zobrazit místní verze:',
specificVersion: 'Specifická verze',
status: 'Stav',
unpublish: 'Zrušit publikování',
unpublishing: 'Zrušuji publikování...',
version: 'Verze',
versionAgo: 'před {{distance}}',
versionCount_many: '{{count}} verzí nalezeno',
versionCount_none: 'Žádné verze nenalezeny',
versionCount_one: '{{count}} verze nalezena',

View File

@@ -497,22 +497,28 @@ export const daTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} ændret felt',
changedFieldsCount_other: '{{count}} ændrede felter',
compareVersion: 'Sammenlign version med:',
compareVersions: 'Sammenlign versioner',
comparingAgainst: 'Sammenligner med',
confirmPublish: 'Bekræft offentliggørelse',
confirmRevertToSaved: 'Bekræft tilbagerulning til gemt',
confirmUnpublish: 'Bekræft afpublicering',
confirmVersionRestoration: 'Bekræft versionens gendannelse',
currentDocumentStatus: 'Nuværende {{docStatus}} dokument',
currentDraft: 'Nuværende kladde',
currentlyPublished: 'Aktuelt Offentliggjort',
currentlyViewing: 'Aktuelt visning',
currentPublishedVersion: 'Nuværende offentliggjort version',
draft: 'Kladde',
draftSavedSuccessfully: 'Kladde gemt.',
lastSavedAgo: 'Sidst gemt {{distance}}',
modifiedOnly: 'Kun ændret',
moreVersions: 'Flere versioner...',
noFurtherVersionsFound: 'Ingen yderligere versioner fundet',
noRowsFound: 'Ingen {{label}} fundet',
noRowsSelected: 'Ingen {{label}} valgt',
preview: 'Forhåndsvisning',
previouslyPublished: 'Tidligere offentliggjort',
previousVersion: 'Tidligere version',
problemRestoringVersion: 'Der opstod et problem med at gendanne denne version',
publish: 'Offentliggør',
publishAllLocales: 'Udgiv alle lokalindstillinger',
@@ -533,10 +539,12 @@ export const daTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Vælg en version til sammenligning',
showingVersionsFor: 'Viser versioner for:',
showLocales: 'Vis lokaliteter:',
specificVersion: 'Specifik Version',
status: 'Status',
unpublish: 'Afpublicer',
unpublishing: 'Afpublicerer...',
version: 'Version',
versionAgo: '{{distance}} siden',
versionCount_many: '{{count}} versioner fundet',
versionCount_none: 'Ingen versioner fundet',
versionCount_one: '{{count}} version fundet',

View File

@@ -506,22 +506,28 @@ export const deTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} geändertes Feld',
changedFieldsCount_other: '{{count}} geänderte Felder',
compareVersion: 'Vergleiche Version zu:',
compareVersions: 'Versionen vergleichen',
comparingAgainst: 'Im Vergleich zu',
confirmPublish: 'Veröffentlichung bestätigen',
confirmRevertToSaved: 'Zurücksetzen auf die letzte Speicherung bestätigen',
confirmUnpublish: 'Aufhebung der Veröffentlichung bestätigen',
confirmVersionRestoration: 'Wiederherstellung der Version bestätigen',
currentDocumentStatus: 'Aktueller Dokumentenstatus: {{docStatus}}',
currentDraft: 'Aktueller Entwurf',
currentlyPublished: 'Derzeit veröffentlicht',
currentlyViewing: 'Derzeitige Ansicht',
currentPublishedVersion: 'Aktuell veröffentlichte Version',
draft: 'Entwurf',
draftSavedSuccessfully: 'Entwurf erfolgreich gespeichert.',
lastSavedAgo: 'Zuletzt vor {{distance}} gespeichert',
modifiedOnly: 'Nur modifiziert',
moreVersions: 'Mehr Versionen...',
noFurtherVersionsFound: 'Keine weiteren Versionen vorhanden',
noRowsFound: 'Kein {{label}} gefunden',
noRowsSelected: 'Kein {{label}} ausgewählt',
preview: 'Vorschau',
previouslyPublished: 'Zuvor veröffentlicht',
previousVersion: 'Frühere Version',
problemRestoringVersion: 'Bei der Wiederherstellung der Version ist ein Fehler aufgetreten',
publish: 'Veröffentlichen',
publishAllLocales: 'Alle Sprachen veröffentlichen',
@@ -542,10 +548,12 @@ export const deTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Wähle Version zum Vergleich',
showingVersionsFor: 'Versionen anzeigen für:',
showLocales: 'Sprachen anzeigen:',
specificVersion: 'Spezifische Version',
status: 'Status',
unpublish: 'Veröffentlichung aufheben',
unpublishing: 'Veröffentlichung aufheben...',
version: 'Version',
versionAgo: 'vor {{distance}}',
versionCount_many: '{{count}} Versionen gefunden',
versionCount_none: 'Keine Versionen gefunden',
versionCount_one: '{{count}} Version gefunden',

View File

@@ -500,22 +500,28 @@ export const enTranslations = {
changedFieldsCount_one: '{{count}} changed field',
changedFieldsCount_other: '{{count}} changed fields',
compareVersion: 'Compare version against:',
compareVersions: 'Compare Versions',
comparingAgainst: 'Comparing against',
confirmPublish: 'Confirm publish',
confirmRevertToSaved: 'Confirm revert to saved',
confirmUnpublish: 'Confirm unpublish',
confirmVersionRestoration: 'Confirm version Restoration',
confirmVersionRestoration: 'Confirm Version Restoration',
currentDocumentStatus: 'Current {{docStatus}} document',
currentDraft: 'Current Draft',
currentlyPublished: 'Currently Published',
currentlyViewing: 'Currently viewing',
currentPublishedVersion: 'Current Published Version',
draft: 'Draft',
draftSavedSuccessfully: 'Draft saved successfully.',
lastSavedAgo: 'Last saved {{distance}} ago',
modifiedOnly: 'Modified only',
moreVersions: 'More versions...',
noFurtherVersionsFound: 'No further versions found',
noRowsFound: 'No {{label}} found',
noRowsSelected: 'No {{label}} selected',
preview: 'Preview',
previouslyPublished: 'Previously Published',
previousVersion: 'Previous Version',
problemRestoringVersion: 'There was a problem restoring this version',
publish: 'Publish',
publishAllLocales: 'Publish all locales',
@@ -536,10 +542,12 @@ export const enTranslations = {
selectVersionToCompare: 'Select a version to compare',
showingVersionsFor: 'Showing versions for:',
showLocales: 'Show locales:',
specificVersion: 'Specific Version',
status: 'Status',
unpublish: 'Unpublish',
unpublishing: 'Unpublishing...',
version: 'Version',
versionAgo: '{{distance}} ago',
versionCount_many: '{{count}} versions found',
versionCount_none: 'No versions found',
versionCount_one: '{{count}} version found',

View File

@@ -504,22 +504,28 @@ export const esTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} campo modificado',
changedFieldsCount_other: '{{count}} campos modificados',
compareVersion: 'Comparar versión con:',
compareVersions: 'Comparar Versiones',
comparingAgainst: 'Comparando contra',
confirmPublish: 'Confirmar publicación',
confirmRevertToSaved: 'Confirmar revertir a guardado',
confirmUnpublish: 'Confirmar despublicación',
confirmVersionRestoration: 'Confirmar restauración de versión',
currentDocumentStatus: 'Documento actual: {{docStatus}}',
currentDraft: 'Borrador actual',
currentlyPublished: 'Actualmente Publicado',
currentlyViewing: 'Actualmente viendo',
currentPublishedVersion: 'Versión publicada actual',
draft: 'Borrador',
draftSavedSuccessfully: 'Borrador guardado con éxito.',
lastSavedAgo: 'Guardado por última vez hace {{distance}}',
modifiedOnly: 'Modificado solamente',
moreVersions: 'Más versiones...',
noFurtherVersionsFound: 'No se encontraron más versiones',
noRowsFound: 'No se encontraron {{label}}.',
noRowsSelected: 'No se ha seleccionado ningún {{label}}.',
preview: 'Vista previa',
previouslyPublished: 'Publicado anteriormente',
previousVersion: 'Versión Anterior',
problemRestoringVersion: 'Hubo un problema al restaurar esta versión',
publish: 'Publicar',
publishAllLocales: 'Publicar en todos los idiomas',
@@ -540,10 +546,12 @@ export const esTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Seleccionar una versión para comparar',
showingVersionsFor: 'Mostrando versiones para:',
showLocales: 'Mostrar idiomas:',
specificVersion: 'Versión Específica',
status: 'Estado',
unpublish: 'Despublicar',
unpublishing: 'Despublicando...',
version: 'Versión',
versionAgo: 'hace {{distance}}',
versionCount_many: '{{count}} versiones encontradas',
versionCount_none: 'No se encontraron versiones',
versionCount_one: '{{count}} versión encontrada',

View File

@@ -492,22 +492,28 @@ export const etTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} muudetud väli',
changedFieldsCount_other: '{{count}} muudetud välja',
compareVersion: 'Võrdle versiooni:',
compareVersions: 'Võrdle versioone',
comparingAgainst: 'Võrreldes vastu',
confirmPublish: 'Kinnita avaldamine',
confirmRevertToSaved: 'Kinnita taastamine salvestatud seisundisse',
confirmUnpublish: 'Kinnita avaldamise tühistamine',
confirmVersionRestoration: 'Kinnita versiooni taastamine',
currentDocumentStatus: 'Praegune {{docStatus}} dokument',
currentDraft: 'Praegune mustand',
currentlyPublished: 'Praegu avaldatud',
currentlyViewing: 'Praegu vaatamine',
currentPublishedVersion: 'Praegune avaldatud versioon',
draft: 'Mustand',
draftSavedSuccessfully: 'Mustand edukalt salvestatud.',
lastSavedAgo: 'Viimati salvestatud {{distance}} tagasi',
modifiedOnly: 'Muudetud ainult',
moreVersions: 'Rohkem versioone...',
noFurtherVersionsFound: 'Rohkem versioone ei leitud',
noRowsFound: '{{label}} ei leitud',
noRowsSelected: '{{label}} pole valitud',
preview: 'Eelvaade',
previouslyPublished: 'Varem avaldatud',
previousVersion: 'Eelmine versioon',
problemRestoringVersion: 'Selle versiooni taastamisel tekkis probleem',
publish: 'Avalda',
publishAllLocales: 'Avaldage kõik lokaadid',
@@ -528,10 +534,12 @@ export const etTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Vali versioon võrdlemiseks',
showingVersionsFor: 'Näitan versioone:',
showLocales: 'Näita keeli:',
specificVersion: 'Spetsiifiline versioon',
status: 'Olek',
unpublish: 'Tühista avaldamine',
unpublishing: 'Avaldamise tühistamine...',
version: 'Versioon',
versionAgo: '{{distance}} tagasi',
versionCount_many: '{{count}} versiooni leitud',
versionCount_none: 'Versioone ei leitud',
versionCount_one: '{{count}} versioon leitud',

View File

@@ -495,22 +495,28 @@ export const faTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} فیلد تغییر کرد',
changedFieldsCount_other: '{{count}} فیلدهای تغییر یافته',
compareVersion: 'مقایسه نگارش با:',
compareVersions: 'مقایسه نسخه ها',
comparingAgainst: 'مقایسه با',
confirmPublish: 'تأیید انتشار',
confirmRevertToSaved: 'تأیید بازگردانی نگارش ذخیره شده',
confirmUnpublish: 'تأیید لغو انتشار',
confirmVersionRestoration: 'تأیید بازیابی نگارش',
currentDocumentStatus: 'جاری {{docStatus}} سند',
currentDraft: 'پیش نویس فعلی',
currentlyPublished: 'منتشر شده است',
currentlyViewing: 'در حال مشاهده',
currentPublishedVersion: 'نسخه منتشر شده فعلی',
draft: 'پیش‌نویس',
draftSavedSuccessfully: 'پیش‌نویس با موفقیت ذخیره شد.',
lastSavedAgo: 'آخرین بار {{distance}} پیش ذخیره شد',
modifiedOnly: 'تنها تغییر یافته',
moreVersions: 'نسخه های بیشتر...',
noFurtherVersionsFound: 'نگارش دیگری یافت نشد',
noRowsFound: 'هیچ {{label}} یافت نشد',
noRowsSelected: 'هیچ {{label}} ای انتخاب نشده است',
preview: 'پیش‌نمایش',
previouslyPublished: 'قبلا منتشر شده',
previousVersion: 'نسخه قبلی',
problemRestoringVersion: 'مشکلی در بازیابی این نگارش وجود دارد',
publish: 'انتشار',
publishAllLocales: 'انتشار در تمام مکان‌های محلی',
@@ -531,10 +537,12 @@ export const faTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'نگارشی را برای مقایسه انتخاب کنید',
showingVersionsFor: 'نمایش نگارش‌ها برای:',
showLocales: 'نمایش زبان‌ها:',
specificVersion: 'نسخه مشخص',
status: 'وضعیت',
unpublish: 'لغو انتشار',
unpublishing: 'در حال لغو انتشار...',
version: 'نگارش',
versionAgo: '{{distance}} پیش',
versionCount_many: '{{count}} نگارش‌ یافت شد',
versionCount_none: 'هیچ نگارشی یافت نشد',
versionCount_one: '{{count}} نگارش یافت شد',

View File

@@ -512,22 +512,28 @@ export const frTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} champ modifié',
changedFieldsCount_other: '{{count}} champs modifiés',
compareVersion: 'Comparez cette version à :',
compareVersions: 'Comparer les versions',
comparingAgainst: 'En comparaison avec',
confirmPublish: 'Confirmer la publication',
confirmRevertToSaved: 'Confirmer la restauration',
confirmUnpublish: 'Confirmer lannulation',
confirmVersionRestoration: 'Confirmer la restauration de la version',
currentDocumentStatus: 'Document {{docStatus}} actuel',
currentDraft: 'Projet actuel',
currentlyPublished: 'Actuellement publié',
currentlyViewing: 'Actuellement en train de regarder',
currentPublishedVersion: 'Version Publiée Actuelle',
draft: 'Brouillon',
draftSavedSuccessfully: 'Brouillon enregistré avec succès.',
lastSavedAgo: 'Dernière sauvegarde il y a {{distance}}',
modifiedOnly: 'Modifié uniquement',
moreVersions: 'Plus de versions...',
noFurtherVersionsFound: 'Aucune autre version trouvée',
noRowsFound: 'Aucun(e) {{label}} trouvé(e)',
noRowsSelected: 'Aucune {{étiquette}} sélectionnée',
preview: 'Aperçu',
previouslyPublished: 'Précédemment publié',
previousVersion: 'Version Précédente',
problemRestoringVersion: 'Un problème est survenu lors de la restauration de cette version',
publish: 'Publier',
publishAllLocales: 'Publier toutes les localités',
@@ -548,10 +554,12 @@ export const frTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Sélectionnez une version à comparer',
showingVersionsFor: 'Affichage des versions pour :',
showLocales: 'Afficher les paramètres régionaux :',
specificVersion: 'Version spécifique',
status: 'Statut',
unpublish: 'Annuler la publication',
unpublishing: 'Annulation en cours...',
version: 'Version',
versionAgo: 'il y a {{distance}}',
versionCount_many: '{{count}} versions trouvées',
versionCount_none: 'Aucune version trouvée',
versionCount_one: '{{count}} version trouvée',

View File

@@ -483,22 +483,28 @@ export const heTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} שינה שדה',
changedFieldsCount_other: '{{count}} שדות ששונו',
compareVersion: 'השווה לגרסה:',
compareVersions: 'השווה גרסאות',
comparingAgainst: 'השוואה לעומת',
confirmPublish: 'אישור פרסום',
confirmRevertToSaved: 'אישור שחזור לגרסה שנשמרה',
confirmUnpublish: 'אישור ביטול פרסום',
confirmVersionRestoration: 'אישור שחזור גרסה',
currentDocumentStatus: 'מסמך {{docStatus}} נוכחי',
currentDraft: 'טיוטה נוכחית',
currentlyPublished: 'פורסם כרגע',
currentlyViewing: 'מציג כרגע',
currentPublishedVersion: 'הגרסה שפורסמה כעת',
draft: 'טיוטה',
draftSavedSuccessfully: 'טיוטה נשמרה בהצלחה.',
lastSavedAgo: 'נשמר לאחרונה לפני {{distance}}',
modifiedOnly: 'מותאם בלבד',
moreVersions: 'עוד גרסאות...',
noFurtherVersionsFound: 'לא נמצאו עוד גרסאות',
noRowsFound: 'לא נמצאו {{label}}',
noRowsSelected: 'לא נבחר {{תווית}}',
preview: 'תצוגה מקדימה',
previouslyPublished: 'פורסם בעבר',
previousVersion: 'גרסה קודמת',
problemRestoringVersion: 'הייתה בעיה בשחזור הגרסה הזו',
publish: 'פרסם',
publishAllLocales: 'פרסם את כל המיקומים',
@@ -519,10 +525,12 @@ export const heTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'בחר גרסה להשוואה',
showingVersionsFor: 'מציג גרסאות עבור:',
showLocales: 'הצג שפות:',
specificVersion: 'גרסה מסוימת',
status: 'סטטוס',
unpublish: 'בטל פרסום',
unpublishing: 'מבטל פרסום...',
version: 'גרסה',
versionAgo: 'לפני {{distance}}',
versionCount_many: '{{count}} גרסאות נמצאו',
versionCount_none: 'לא נמצאו גרסאות',
versionCount_one: 'נמצאה גרסה אחת',

View File

@@ -496,22 +496,28 @@ export const hrTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} promijenjeno polje',
changedFieldsCount_other: '{{count}} promijenjena polja',
compareVersion: 'Usporedi verziju sa:',
compareVersions: 'Usporedi verzije',
comparingAgainst: 'U usporedbi s',
confirmPublish: 'Potvrdi objavu',
confirmRevertToSaved: 'Potvrdite vraćanje na spremljeno',
confirmUnpublish: 'Potvrdite poništavanje objave',
confirmVersionRestoration: 'Potvrdite vraćanje verzije',
currentDocumentStatus: 'Trenutni {{docStatus}} dokumenta',
currentDraft: 'Trenutni Nacrt',
currentlyPublished: 'Trenutno objavljeno',
currentlyViewing: 'Trenutno pregledavate',
currentPublishedVersion: 'Trenutno Objavljena Verzija',
draft: 'Nacrt',
draftSavedSuccessfully: 'Nacrt uspješno spremljen.',
lastSavedAgo: 'Zadnji put spremljeno prije {{distance}',
modifiedOnly: 'Samo modificirano',
moreVersions: 'Više verzija...',
noFurtherVersionsFound: 'Nisu pronađene daljnje verzije',
noRowsFound: '{{label}} nije pronađeno',
noRowsSelected: 'Nije odabrana {{oznaka}}',
preview: 'Pregled',
previouslyPublished: 'Prethodno objavljeno',
previousVersion: 'Prethodna verzija',
problemRestoringVersion: 'Nastao je problem pri vraćanju ove verzije',
publish: 'Objaviti',
publishAllLocales: 'Objavi sve lokalne postavke',
@@ -532,10 +538,12 @@ export const hrTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Odaberite verziju za usporedbu',
showingVersionsFor: 'Pokazujem verzije za:',
showLocales: 'Prikaži jezike:',
specificVersion: 'Specifična verzija',
status: 'Status',
unpublish: 'Poništi objavu',
unpublishing: 'Poništavanje objave...',
version: 'Verzija',
versionAgo: 'prije {{distance}}',
versionCount_many: '{{count}} pronađenih verzija',
versionCount_none: 'Nema pronađenih verzija',
versionCount_one: '{{count}} pronađena verzija',

View File

@@ -503,22 +503,28 @@ export const huTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} megváltozott mező',
changedFieldsCount_other: '{{count}} módosított mező',
compareVersion: 'Hasonlítsa össze a verziót a következőkkel:',
compareVersions: 'Verziók összehasonlítása',
comparingAgainst: 'Összehasonlítva',
confirmPublish: 'A közzététel megerősítése',
confirmRevertToSaved: 'Erősítse meg a mentett verzióra való visszatérést',
confirmUnpublish: 'A közzététel visszavonásának megerősítése',
confirmVersionRestoration: 'Verzió-visszaállítás megerősítése',
currentDocumentStatus: 'Jelenlegi {{docStatus}} dokumentum',
currentDraft: 'Aktuális tervezet',
currentlyPublished: 'Jelenleg közzétéve',
currentlyViewing: 'Jelenlegi megtekintés',
currentPublishedVersion: 'Jelenleg Közzétett Verzió',
draft: 'Piszkozat',
draftSavedSuccessfully: 'A piszkozat sikeresen mentve.',
lastSavedAgo: 'Utoljára mentve {{distance}} órája',
modifiedOnly: 'Módosítva csak',
moreVersions: 'További verziók...',
noFurtherVersionsFound: 'További verziók nem találhatók',
noRowsFound: 'Nem található {{label}}',
noRowsSelected: 'Nincs {{címke}} kiválasztva',
preview: 'Előnézet',
previouslyPublished: 'Korábban Közzétéve',
previousVersion: 'Előző Verzió',
problemRestoringVersion: 'Hiba történt a verzió visszaállításakor',
publish: 'Közzététel',
publishAllLocales: 'Közzétesz az összes helyszínen',
@@ -539,10 +545,12 @@ export const huTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Válassza ki az összehasonlítani kívánt verziót',
showingVersionsFor: 'Verziók megjelenítése a következőkhöz:',
showLocales: 'Nyelvek megjelenítése:',
specificVersion: 'Specifikus verzió',
status: 'Állapot',
unpublish: 'Közzététel visszavonása',
unpublishing: 'Közzététel visszavonása...',
version: 'Verzió',
versionAgo: '{{distance}} ezelőtt',
versionCount_many: '{{count}} verzió található',
versionCount_none: 'Nem található verzió',
versionCount_one: '{{count}} verzió található',

View File

@@ -506,22 +506,28 @@ export const hyTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} փոփոխված դաշտ',
changedFieldsCount_other: '{{count}} փոփոխված դաշտեր',
compareVersion: 'Համեմատել տարբերակը հետևյալի հետ՝',
compareVersions: 'Համեմատել տարբերակները',
comparingAgainst: 'Համեմատել հետևյալի հետ',
confirmPublish: 'Հաստատել հրապարակումը',
confirmRevertToSaved: 'Հաստատել վերադարձը պահպանված վիճակին',
confirmUnpublish: 'Հաստատել վերադարձը չհրապարակված վիճակին։',
confirmVersionRestoration: 'Հաստատել տարբերակի վերականգնումը',
currentDocumentStatus: 'Ընթացիկ {{docStatus}} փաստաթուղթ',
currentDraft: 'Ընթացիկ սևագիր',
currentlyPublished: 'Ներկայումս հրատարակված',
currentlyViewing: 'Ներկայումս դիտում է',
currentPublishedVersion: 'Ընթացիկ հրապարակված տարբերակ',
draft: 'Սևագիր',
draftSavedSuccessfully: 'Սևագիրը հաջողությամբ պահպանվել է։',
lastSavedAgo: 'Վերջին անգամ պահպանվել է {{distance}} առաջ',
modifiedOnly: 'Միայն փոփոխված',
moreVersions: 'Ավելի շատ տարբերակներ...',
noFurtherVersionsFound: 'Այլ տարբերակներ չեն գտնվել',
noRowsFound: ' Ոչ մի {{label}} չի գտնվել',
noRowsSelected: 'Ոչ մի {{label}} ընտրված չէ',
preview: 'Նախադիտում',
previouslyPublished: 'Նախկինում հրապարակված',
previousVersion: 'Նախորդ Տարբերակ',
problemRestoringVersion: 'Այս տարբերակը վերականգնելու ժամանակ խնդիր է առաջացել',
publish: 'Հրապարակել',
publishAllLocales: 'Հրապարակել բոլոր լոկալներում',
@@ -542,10 +548,12 @@ export const hyTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Ընտրեք տարբերակ՝ համեմատելու համար',
showingVersionsFor: 'Ցուցադրված են տարբերակները՝',
showLocales: 'Ցուցադրել լոկալները՝',
specificVersion: 'Մասնավոր Տարբերակ',
status: 'Կարգավիճակ',
unpublish: 'Բերել չհրապարակված վիճակի։',
unpublishing: 'Բերվում է չհրապարակված վիճակի...',
version: 'Տարբերակ',
versionAgo: '{{distance}} առաջ',
versionCount_many: 'Գտնվել են {{count}} տարբերակներ',
versionCount_none: 'Ոչ մի տարբերակ չի գտնվել',
versionCount_one: '{{count}} տարբերակ է գտնվել',

View File

@@ -503,22 +503,28 @@ export const itTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} campo modificato',
changedFieldsCount_other: '{{count}} campi modificati',
compareVersion: 'Confronta versione con:',
compareVersions: 'Confronta Versioni',
comparingAgainst: 'Confrontando con',
confirmPublish: 'Conferma la pubblicazione',
confirmRevertToSaved: 'Conferma il ripristino dei salvataggi',
confirmUnpublish: 'Conferma annullamento della pubblicazione',
confirmVersionRestoration: 'Conferma il ripristino della versione',
currentDocumentStatus: 'Documento {{docStatus}} corrente',
currentDraft: 'Bozza Corrente',
currentlyPublished: 'Attualmente Pubblicato',
currentlyViewing: 'Visualizzazione attuale',
currentPublishedVersion: 'Versione Pubblicata Attuale',
draft: 'Bozza',
draftSavedSuccessfully: 'Bozza salvata con successo.',
lastSavedAgo: 'Ultimo salvataggio {{distance}} fa',
modifiedOnly: 'Modificato solo',
moreVersions: 'Altre versioni...',
noFurtherVersionsFound: 'Non sono state trovate ulteriori versioni',
noRowsFound: 'Nessun {{label}} trovato',
noRowsSelected: 'Nessuna {{etichetta}} selezionata',
preview: 'Anteprima',
previouslyPublished: 'Precedentemente Pubblicato',
previousVersion: 'Versione Precedente',
problemRestoringVersion: 'Si è verificato un problema durante il ripristino di questa versione',
publish: 'Pubblicare',
publishAllLocales: 'Pubblica tutte le località',
@@ -539,10 +545,12 @@ export const itTranslations: DefaultTranslationsObject = {
selectVersionToCompare: 'Seleziona una versione da confrontare',
showingVersionsFor: 'Mostra le versioni per:',
showLocales: 'Mostra localizzazioni:',
specificVersion: 'Versione Specifica',
status: 'Stato',
unpublish: 'Annulla pubblicazione',
unpublishing: 'Annullamento pubblicazione...',
version: 'Versione',
versionAgo: '{{distance}} fa',
versionCount_many: '{{count}} versioni trovate',
versionCount_none: 'Nessuna versione trovata',
versionCount_one: '{{count}} versione trovata',

View File

@@ -497,22 +497,28 @@ export const jaTranslations: DefaultTranslationsObject = {
changedFieldsCount_one: '{{count}} 変更されたフィールド',
changedFieldsCount_other: '{{count}}つの変更されたフィールド',
compareVersion: 'バージョンを比較:',
compareVersions: 'バージョンを比較する',
comparingAgainst: '比較対象とする',
confirmPublish: '公開を確認する',
confirmRevertToSaved: '保存された状態に戻す確認',
confirmUnpublish: '非公開の確認',
confirmVersionRestoration: 'バージョン復元の確認',
currentDocumentStatus: '現在の {{docStatus}} データ',
currentDraft: '現行の草案',
currentlyPublished: '現在公開中',
currentlyViewing: '現在表示中',
currentPublishedVersion: '現在公開されているバージョン',
draft: 'ドラフト',
draftSavedSuccessfully: '下書きは正常に保存されました。',
lastSavedAgo: '{{distance}}前に最後に保存されました',
modifiedOnly: '変更済みのみ',
moreVersions: 'さらに多くのバージョン...',
noFurtherVersionsFound: 'その他のバージョンは見つかりませんでした。',
noRowsFound: '{{label}} は未設定です',
noRowsSelected: '選択された{{label}}はありません',
preview: 'プレビュー',
previouslyPublished: '以前に公開された',
previousVersion: '以前のバージョン',
problemRestoringVersion: 'このバージョンの復元に問題がありました。',
publish: '公開する',
publishAllLocales: 'すべてのロケールを公開する',
@@ -533,10 +539,12 @@ export const jaTranslations: DefaultTranslationsObject = {
selectVersionToCompare: '比較するバージョンを選択',
showingVersionsFor: '次のバージョンを表示します:',
showLocales: 'ロケールを表示:',
specificVersion: '特定のバージョン',
status: 'ステータス',
unpublish: '非公開',
unpublishing: '非公開中...',
version: 'バージョン',
versionAgo: '{{distance}}前',
versionCount_many: '{{count}} バージョンがあります',
versionCount_none: 'バージョンがありません',
versionCount_one: '{{count}} バージョンがあります',

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