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   ### After    
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -148,6 +148,7 @@ export async function Account({ initPageResult, params, searchParams }: AdminVie
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
doc: data,
|
||||
hasPublishedDoc,
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export const diffMethods = {
|
||||
radio: 'WORDS_WITH_SPACE',
|
||||
relationship: 'WORDS_WITH_SPACE',
|
||||
select: 'WORDS_WITH_SPACE',
|
||||
upload: 'WORDS_WITH_SPACE',
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })}
|
||||
|
||||
{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>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
@layer payload-default {
|
||||
.select-version-locales {
|
||||
flex-grow: 1;
|
||||
|
||||
&__label {
|
||||
margin-bottom: calc(var(--base) * 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { OptionObject } from 'payload'
|
||||
|
||||
export type Props = {
|
||||
onChange: (options: OptionObject[]) => void
|
||||
options: OptionObject[]
|
||||
value: OptionObject[]
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
26
packages/next/src/views/Version/VersionPillLabel/index.scss
Normal file
26
packages/next/src/views/Version/VersionPillLabel/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
192
packages/next/src/views/Version/fetchVersions.ts
Normal file
192
packages/next/src/views/Version/fetchVersions.ts
Normal 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
|
||||
}
|
||||
@@ -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']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 || {}),
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -625,6 +625,7 @@ export type {
|
||||
DocumentViewServerProps,
|
||||
DocumentViewServerPropsOnly,
|
||||
EditViewProps,
|
||||
RenderDocumentVersionsProperties,
|
||||
} from './views/document.js'
|
||||
|
||||
export type {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
@import '../../colors.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.lexical-diff {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const UnknownDiffHTMLConverterAsync: (args: {
|
||||
)
|
||||
|
||||
// Render to HTML
|
||||
const html = ReactDOMServer.renderToString(JSX)
|
||||
const html = ReactDOMServer.renderToStaticMarkup(JSX)
|
||||
|
||||
return html
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}} من النّسخ',
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
@@ -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}} открита версия',
|
||||
|
||||
@@ -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}}টি সংস্করণ পাওয়া গেছে',
|
||||
|
||||
@@ -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}}টি সংস্করণ পাওয়া গেছে',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}} نگارش یافت شد',
|
||||
|
||||
@@ -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 l’annulation',
|
||||
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',
|
||||
|
||||
@@ -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: 'נמצאה גרסה אחת',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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ó',
|
||||
|
||||
@@ -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}} տարբերակ է գտնվել',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user