feat: adds new experimental.localizeStatus option (#13207)
### What? Adds a new `experimental.localizeStatus` config option, set to `false` by default. When `true`, the admin panel will display the document status based on the *current locale* instead of the _latest_ overall status. Also updates the edit view to only show a `changed` status when `autosave` is enabled. ### Why? Showing the status for the current locale is more accurate and useful in multi-locale setups. This update will become default behavior, able to be opted in by setting `experimental.localizeStatus: true` in the Payload config. This option will become depreciated in V4. ### How? When `localizeStatus` is `true`, we store the localized status in a new `localeStatus` field group within version data. The admin panel then reads from this field to display the correct status for the current locale. --------- Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
This commit is contained in:
@@ -131,6 +131,29 @@ localization: {
|
|||||||
|
|
||||||
Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.
|
Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.
|
||||||
|
|
||||||
|
## Experimental Options
|
||||||
|
|
||||||
|
Experimental options are features that may not be fully stable and may change or be removed in future releases.
|
||||||
|
|
||||||
|
These options can be enabled in your Payload Config under the `experimental` key. You can set them like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { buildConfig } from 'payload'
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
// ...
|
||||||
|
experimental: {
|
||||||
|
localizeStatus: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The following experimental options are available related to localization:
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
|
||||||
|
|
||||||
## Field Localization
|
## Field Localization
|
||||||
|
|
||||||
Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.
|
Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ The following options are available:
|
|||||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||||
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
|
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
|
||||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||||
|
| **`experimental`** | Configure experimental features for Payload. These may be unstable and may change or be removed in future releases. [More details](../experimental). |
|
||||||
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||||
|
|||||||
66
docs/experimental/overview.mdx
Normal file
66
docs/experimental/overview.mdx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
title: Experimental Features
|
||||||
|
label: Overview
|
||||||
|
order: 10
|
||||||
|
desc: Enable and configure experimental functionality within Payload. These featuresmay be unstable and may change or be removed without notice.
|
||||||
|
keywords: experimental, unstable, beta, preview, features, configuration, Payload, cms, headless, javascript, node, react, nextjs
|
||||||
|
---
|
||||||
|
|
||||||
|
Experimental features allow you to try out new functionality before it becomes a stable part of Payload. These features may still be in active development, may have incomplete functionality, and can change or be removed in future releases without warning.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Experimental features are configured via the root-level `experimental` property in your [Payload Config](../configuration/overview). This property contains individual feature flags, each flag can be configured independently, allowing you to selectively opt into specific functionality.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { buildConfig } from 'payload'
|
||||||
|
|
||||||
|
const config = buildConfig({
|
||||||
|
// ...
|
||||||
|
experimental: {
|
||||||
|
localizeStatus: true, // highlight-line
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Experimental Options
|
||||||
|
|
||||||
|
The following options are available:
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
|
||||||
|
|
||||||
|
This list may change without notice.
|
||||||
|
|
||||||
|
## When to Use Experimental Features
|
||||||
|
|
||||||
|
You might enable an experimental feature when:
|
||||||
|
|
||||||
|
- You want early access to new capabilities before their stable release.
|
||||||
|
- You can accept the risks of using potentially unstable functionality.
|
||||||
|
- You are testing new features in a development or staging environment.
|
||||||
|
- You wish to provide feedback to the Payload team on new functionality.
|
||||||
|
|
||||||
|
If you are working on a production application, carefully evaluate whether the benefits outweigh the risks. For most stable applications, it is recommended to wait until the feature is officially released.
|
||||||
|
|
||||||
|
<Banner type="success">
|
||||||
|
<strong>Tip:</strong> To stay up to date on experimental features or share
|
||||||
|
your feedback, visit the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/payloadcms/payload/discussions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Payload GitHub Discussions
|
||||||
|
</a>{' '}
|
||||||
|
or{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/payloadcms/payload/issues"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
open an issue
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</Banner>
|
||||||
@@ -12,6 +12,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
|||||||
autosave,
|
autosave,
|
||||||
createdAt,
|
createdAt,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
|
localeStatus,
|
||||||
parent,
|
parent,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
req,
|
req,
|
||||||
@@ -33,6 +34,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
|||||||
autosave,
|
autosave,
|
||||||
createdAt,
|
createdAt,
|
||||||
latest: true,
|
latest: true,
|
||||||
|
localeStatus,
|
||||||
parent,
|
parent,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
|||||||
autosave,
|
autosave,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
localeStatus,
|
||||||
parent,
|
parent,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
req,
|
req,
|
||||||
@@ -37,6 +38,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
|||||||
autosave,
|
autosave,
|
||||||
createdAt,
|
createdAt,
|
||||||
latest: true,
|
latest: true,
|
||||||
|
localeStatus,
|
||||||
parent,
|
parent,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
|||||||
@@ -179,6 +179,13 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
|||||||
|
|
||||||
for (let i = 0; i < result.docs.length; i++) {
|
for (let i = 0; i < result.docs.length; i++) {
|
||||||
const id = result.docs[i].parent
|
const id = result.docs[i].parent
|
||||||
|
|
||||||
|
const localeStatus = result.docs[i].localeStatus || {}
|
||||||
|
if (locale && localeStatus[locale]) {
|
||||||
|
result.docs[i].status = localeStatus[locale]
|
||||||
|
result.docs[i].version._status = localeStatus[locale]
|
||||||
|
}
|
||||||
|
|
||||||
result.docs[i] = result.docs[i].version ?? {}
|
result.docs[i] = result.docs[i].version ?? {}
|
||||||
result.docs[i].id = id
|
result.docs[i].id = id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
|||||||
autosave,
|
autosave,
|
||||||
createdAt,
|
createdAt,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
|
localeStatus,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
req,
|
req,
|
||||||
returning,
|
returning,
|
||||||
@@ -35,6 +36,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
|||||||
autosave,
|
autosave,
|
||||||
createdAt,
|
createdAt,
|
||||||
latest: true,
|
latest: true,
|
||||||
|
localeStatus,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
snapshot,
|
snapshot,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export async function createVersion<T extends TypeWithID>(
|
|||||||
autosave,
|
autosave,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
localeStatus,
|
||||||
parent,
|
parent,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
req,
|
req,
|
||||||
@@ -40,6 +41,7 @@ export async function createVersion<T extends TypeWithID>(
|
|||||||
autosave,
|
autosave,
|
||||||
createdAt,
|
createdAt,
|
||||||
latest: true,
|
latest: true,
|
||||||
|
localeStatus,
|
||||||
parent,
|
parent,
|
||||||
publishedLocale,
|
publishedLocale,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
|||||||
@@ -36,15 +36,17 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
|||||||
where: combinedWhere,
|
where: combinedWhere,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
for (let i = 0; i < result.docs.length; i++) {
|
||||||
...result,
|
const id = result.docs[i].parent
|
||||||
docs: result.docs.map((doc) => {
|
const localeStatus = result.docs[i].localeStatus || {}
|
||||||
doc = {
|
if (locale && localeStatus[locale]) {
|
||||||
id: doc.parent,
|
result.docs[i].status = localeStatus[locale]
|
||||||
...doc.version,
|
result.docs[i].version._status = localeStatus[locale]
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc
|
result.docs[i] = result.docs[i].version ?? {}
|
||||||
}),
|
result.docs[i].id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type AutosaveCellProps = {
|
|||||||
rowData: {
|
rowData: {
|
||||||
autosave?: boolean
|
autosave?: boolean
|
||||||
id: number | string
|
id: number | string
|
||||||
|
localeStatus?: Record<string, 'draft' | 'published'>
|
||||||
publishedLocale?: string
|
publishedLocale?: string
|
||||||
version: {
|
version: {
|
||||||
_status: string
|
_status: string
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ export const createOperation = async <
|
|||||||
autosave,
|
autosave,
|
||||||
collection: collectionConfig,
|
collection: collectionConfig,
|
||||||
docWithLocales: result,
|
docWithLocales: result,
|
||||||
|
locale,
|
||||||
operation: 'create',
|
operation: 'create',
|
||||||
payload,
|
payload,
|
||||||
publishSpecificLocale,
|
publishSpecificLocale,
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ export const updateDocument = async <
|
|||||||
collection: collectionConfig,
|
collection: collectionConfig,
|
||||||
docWithLocales: result,
|
docWithLocales: result,
|
||||||
draft: shouldSaveDraft,
|
draft: shouldSaveDraft,
|
||||||
|
locale,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
payload,
|
payload,
|
||||||
publishSpecificLocale,
|
publishSpecificLocale,
|
||||||
|
|||||||
@@ -159,6 +159,16 @@ export const createClientConfig = ({
|
|||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'experimental':
|
||||||
|
if (config.experimental) {
|
||||||
|
clientConfig.experimental = {}
|
||||||
|
if (config.experimental?.localizeStatus) {
|
||||||
|
clientConfig.experimental.localizeStatus = config.experimental.localizeStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
case 'folders':
|
case 'folders':
|
||||||
if (config.folders) {
|
if (config.folders) {
|
||||||
clientConfig.folders = {
|
clientConfig.folders = {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
|||||||
defaultDepth: 2,
|
defaultDepth: 2,
|
||||||
defaultMaxTextLength: 40000,
|
defaultMaxTextLength: 40000,
|
||||||
endpoints: [],
|
endpoints: [],
|
||||||
|
experimental: {},
|
||||||
globals: [],
|
globals: [],
|
||||||
graphQL: {
|
graphQL: {
|
||||||
disablePlaygroundInProduction: true,
|
disablePlaygroundInProduction: true,
|
||||||
@@ -121,6 +122,7 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
|||||||
config.defaultDepth = config.defaultDepth ?? 2
|
config.defaultDepth = config.defaultDepth ?? 2
|
||||||
config.defaultMaxTextLength = config.defaultMaxTextLength ?? 40000
|
config.defaultMaxTextLength = config.defaultMaxTextLength ?? 40000
|
||||||
config.endpoints = config.endpoints ?? []
|
config.endpoints = config.endpoints ?? []
|
||||||
|
config.experimental = config.experimental ?? {}
|
||||||
config.globals = config.globals ?? []
|
config.globals = config.globals ?? []
|
||||||
config.graphQL = {
|
config.graphQL = {
|
||||||
disableIntrospectionInProduction: true,
|
disableIntrospectionInProduction: true,
|
||||||
|
|||||||
@@ -721,6 +721,14 @@ export type ImportMapGenerators = Array<
|
|||||||
}) => void
|
}) => void
|
||||||
>
|
>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimental features.
|
||||||
|
* These may be unstable and may change or be removed in future releases.
|
||||||
|
*/
|
||||||
|
export type ExperimentalConfig = {
|
||||||
|
localizeStatus?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type AfterErrorHook = (
|
export type AfterErrorHook = (
|
||||||
args: AfterErrorHookArgs,
|
args: AfterErrorHookArgs,
|
||||||
) => AfterErrorResult | Promise<AfterErrorResult>
|
) => AfterErrorResult | Promise<AfterErrorResult>
|
||||||
@@ -1041,6 +1049,12 @@ export type Config = {
|
|||||||
email?: EmailAdapter | Promise<EmailAdapter>
|
email?: EmailAdapter | Promise<EmailAdapter>
|
||||||
/** Custom REST endpoints */
|
/** Custom REST endpoints */
|
||||||
endpoints?: Endpoint[]
|
endpoints?: Endpoint[]
|
||||||
|
/**
|
||||||
|
* Configure experimental features for Payload.
|
||||||
|
*
|
||||||
|
* These features may be unstable and may change or be removed in future releases.
|
||||||
|
*/
|
||||||
|
experimental?: ExperimentalConfig
|
||||||
/**
|
/**
|
||||||
* Options for folder view within the admin panel
|
* Options for folder view within the admin panel
|
||||||
* @experimental this feature may change in minor versions until it is fully stable
|
* @experimental this feature may change in minor versions until it is fully stable
|
||||||
@@ -1309,6 +1323,7 @@ export type SanitizedConfig = {
|
|||||||
/** Default richtext editor to use for richText fields */
|
/** Default richtext editor to use for richText fields */
|
||||||
editor?: RichTextAdapter<any, any, any>
|
editor?: RichTextAdapter<any, any, any>
|
||||||
endpoints: Endpoint[]
|
endpoints: Endpoint[]
|
||||||
|
experimental?: ExperimentalConfig
|
||||||
globals: SanitizedGlobalConfig[]
|
globals: SanitizedGlobalConfig[]
|
||||||
i18n: Required<I18nOptions>
|
i18n: Required<I18nOptions>
|
||||||
jobs: SanitizedJobsConfig
|
jobs: SanitizedJobsConfig
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ export type CreateVersionArgs<T = TypeWithID> = {
|
|||||||
autosave: boolean
|
autosave: boolean
|
||||||
collectionSlug: CollectionSlug
|
collectionSlug: CollectionSlug
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
localeStatus?: Record<string, 'draft' | 'published'>
|
||||||
/** ID of the parent document for which the version should be created for */
|
/** ID of the parent document for which the version should be created for */
|
||||||
parent: number | string
|
parent: number | string
|
||||||
publishedLocale?: string
|
publishedLocale?: string
|
||||||
@@ -414,6 +415,7 @@ export type CreateGlobalVersionArgs<T = TypeWithID> = {
|
|||||||
autosave: boolean
|
autosave: boolean
|
||||||
createdAt: string
|
createdAt: string
|
||||||
globalSlug: GlobalSlug
|
globalSlug: GlobalSlug
|
||||||
|
localeStatus?: Record<string, 'draft' | 'published'>
|
||||||
/** ID of the parent document for which the version should be created for */
|
/** ID of the parent document for which the version should be created for */
|
||||||
parent: number | string
|
parent: number | string
|
||||||
publishedLocale?: string
|
publishedLocale?: string
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ export const updateOperation = async <
|
|||||||
docWithLocales: result,
|
docWithLocales: result,
|
||||||
draft: shouldSaveDraft,
|
draft: shouldSaveDraft,
|
||||||
global: globalConfig,
|
global: globalConfig,
|
||||||
|
locale,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
payload,
|
payload,
|
||||||
publishSpecificLocale,
|
publishSpecificLocale,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import type { SanitizedConfig } from '../config/types.js'
|
||||||
import type { CheckboxField, Field, Option } from '../fields/config/types.js'
|
import type { CheckboxField, Field, Option } from '../fields/config/types.js'
|
||||||
|
|
||||||
export const statuses: Option[] = [
|
export const statuses: Option[] = [
|
||||||
@@ -43,3 +44,23 @@ export const versionSnapshotField: CheckboxField = {
|
|||||||
},
|
},
|
||||||
index: true,
|
index: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildLocaleStatusField(config: SanitizedConfig): Field[] {
|
||||||
|
if (!config.localization || !config.localization.locales) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.localization.locales.map((locale) => {
|
||||||
|
const code = typeof locale === 'string' ? locale : locale.code
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: code,
|
||||||
|
type: 'select',
|
||||||
|
index: true,
|
||||||
|
options: [
|
||||||
|
{ label: ({ t }) => t('version:draft'), value: 'draft' },
|
||||||
|
{ label: ({ t }) => t('version:published'), value: 'published' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
|||||||
import type { SanitizedConfig } from '../config/types.js'
|
import type { SanitizedConfig } from '../config/types.js'
|
||||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
import type { Field, FlattenedField } from '../fields/config/types.js'
|
||||||
|
|
||||||
import { versionSnapshotField } from './baseFields.js'
|
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
|
||||||
|
|
||||||
export const buildVersionCollectionFields = <T extends boolean = false>(
|
export const buildVersionCollectionFields = <T extends boolean = false>(
|
||||||
config: SanitizedConfig,
|
config: SanitizedConfig,
|
||||||
@@ -62,6 +62,23 @@ export const buildVersionCollectionFields = <T extends boolean = false>(
|
|||||||
return locale.code
|
return locale.code
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (config.experimental?.localizeStatus) {
|
||||||
|
const localeStatusFields = buildLocaleStatusField(config)
|
||||||
|
|
||||||
|
fields.push({
|
||||||
|
name: 'localeStatus',
|
||||||
|
type: 'group',
|
||||||
|
admin: {
|
||||||
|
disableBulkEdit: true,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
fields: localeStatusFields,
|
||||||
|
...(flatten && {
|
||||||
|
flattenedFields: localeStatusFields as FlattenedField[],
|
||||||
|
})!,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields.push({
|
fields.push({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { SanitizedConfig } from '../config/types.js'
|
|||||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
import type { Field, FlattenedField } from '../fields/config/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||||
|
|
||||||
import { versionSnapshotField } from './baseFields.js'
|
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
|
||||||
|
|
||||||
export const buildVersionGlobalFields = <T extends boolean = false>(
|
export const buildVersionGlobalFields = <T extends boolean = false>(
|
||||||
config: SanitizedConfig,
|
config: SanitizedConfig,
|
||||||
@@ -56,6 +56,23 @@ export const buildVersionGlobalFields = <T extends boolean = false>(
|
|||||||
return locale.code
|
return locale.code
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (config.experimental.localizeStatus) {
|
||||||
|
const localeStatusFields = buildLocaleStatusField(config)
|
||||||
|
|
||||||
|
fields.push({
|
||||||
|
name: 'localeStatus',
|
||||||
|
type: 'group',
|
||||||
|
admin: {
|
||||||
|
disableBulkEdit: true,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
fields: localeStatusFields,
|
||||||
|
...(flatten && {
|
||||||
|
flattenedFields: localeStatusFields as FlattenedField[],
|
||||||
|
})!,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields.push({
|
fields.push({
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ export const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
|||||||
draft.version = {} as T
|
draft.version = {} as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lift locale status from version data if available
|
||||||
|
const localeStatus = draft.localeStatus || {}
|
||||||
|
if (locale && localeStatus[locale]) {
|
||||||
|
;(draft.version as { _status?: string })['_status'] = localeStatus[locale]
|
||||||
|
}
|
||||||
|
|
||||||
// Disregard all other draft content at this point,
|
// Disregard all other draft content at this point,
|
||||||
// Only interested in the version itself.
|
// Only interested in the version itself.
|
||||||
// Operations will handle firing hooks, etc.
|
// Operations will handle firing hooks, etc.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { version } from 'os'
|
||||||
|
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||||
@@ -16,6 +18,7 @@ type Args = {
|
|||||||
draft?: boolean
|
draft?: boolean
|
||||||
global?: SanitizedGlobalConfig
|
global?: SanitizedGlobalConfig
|
||||||
id?: number | string
|
id?: number | string
|
||||||
|
locale?: null | string
|
||||||
operation?: 'create' | 'restoreVersion' | 'update'
|
operation?: 'create' | 'restoreVersion' | 'update'
|
||||||
payload: Payload
|
payload: Payload
|
||||||
publishSpecificLocale?: string
|
publishSpecificLocale?: string
|
||||||
@@ -31,6 +34,7 @@ export const saveVersion = async ({
|
|||||||
docWithLocales: doc,
|
docWithLocales: doc,
|
||||||
draft,
|
draft,
|
||||||
global,
|
global,
|
||||||
|
locale,
|
||||||
operation,
|
operation,
|
||||||
payload,
|
payload,
|
||||||
publishSpecificLocale,
|
publishSpecificLocale,
|
||||||
@@ -42,6 +46,7 @@ export const saveVersion = async ({
|
|||||||
let createNewVersion = true
|
let createNewVersion = true
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const versionData = deepCopyObjectSimple(doc)
|
const versionData = deepCopyObjectSimple(doc)
|
||||||
|
|
||||||
if (draft) {
|
if (draft) {
|
||||||
versionData._status = 'draft'
|
versionData._status = 'draft'
|
||||||
}
|
}
|
||||||
@@ -55,39 +60,39 @@ export const saveVersion = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (autosave) {
|
let docs
|
||||||
let docs
|
const findVersionArgs = {
|
||||||
const findVersionArgs = {
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
req,
|
||||||
|
sort: '-updatedAt',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
;({ docs } = await payload.db.findVersions({
|
||||||
|
...findVersionArgs,
|
||||||
|
collection: collection.slug,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
pagination: false,
|
pagination: false,
|
||||||
req,
|
req,
|
||||||
sort: '-updatedAt',
|
where: {
|
||||||
}
|
parent: {
|
||||||
|
equals: id,
|
||||||
if (collection) {
|
|
||||||
;({ docs } = await payload.db.findVersions({
|
|
||||||
...findVersionArgs,
|
|
||||||
collection: collection.slug,
|
|
||||||
limit: 1,
|
|
||||||
pagination: false,
|
|
||||||
req,
|
|
||||||
where: {
|
|
||||||
parent: {
|
|
||||||
equals: id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}))
|
},
|
||||||
} else {
|
}))
|
||||||
;({ docs } = await payload.db.findGlobalVersions({
|
} else {
|
||||||
...findVersionArgs,
|
;({ docs } = await payload.db.findGlobalVersions({
|
||||||
global: global!.slug,
|
...findVersionArgs,
|
||||||
limit: 1,
|
global: global!.slug,
|
||||||
pagination: false,
|
limit: 1,
|
||||||
req,
|
pagination: false,
|
||||||
}))
|
req,
|
||||||
}
|
}))
|
||||||
const [latestVersion] = docs
|
}
|
||||||
|
const [latestVersion] = docs
|
||||||
|
|
||||||
|
if (autosave) {
|
||||||
// overwrite the latest version if it's set to autosave
|
// overwrite the latest version if it's set to autosave
|
||||||
if (latestVersion && 'autosave' in latestVersion && latestVersion.autosave === true) {
|
if (latestVersion && 'autosave' in latestVersion && latestVersion.autosave === true) {
|
||||||
createNewVersion = false
|
createNewVersion = false
|
||||||
@@ -125,11 +130,53 @@ export const saveVersion = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (createNewVersion) {
|
if (createNewVersion) {
|
||||||
|
let localeStatus = {}
|
||||||
|
const localizationEnabled =
|
||||||
|
payload.config.localization && payload.config.localization.locales.length > 0
|
||||||
|
|
||||||
|
if (
|
||||||
|
localizationEnabled &&
|
||||||
|
payload.config.localization !== false &&
|
||||||
|
payload.config.experimental?.localizeStatus
|
||||||
|
) {
|
||||||
|
const allLocales = (
|
||||||
|
(payload.config.localization && payload.config.localization?.locales) ||
|
||||||
|
[]
|
||||||
|
).map((locale) => (typeof locale === 'string' ? locale : locale.code))
|
||||||
|
|
||||||
|
// If `publish all`, set all locales to published
|
||||||
|
if (versionData._status === 'published' && !publishSpecificLocale) {
|
||||||
|
localeStatus = Object.fromEntries(allLocales.map((code) => [code, 'published']))
|
||||||
|
} else if (publishSpecificLocale || (locale && versionData._status === 'draft')) {
|
||||||
|
const status: 'draft' | 'published' = publishSpecificLocale ? 'published' : 'draft'
|
||||||
|
const incomingLocale = String(publishSpecificLocale || locale)
|
||||||
|
const existing = latestVersion?.localeStatus
|
||||||
|
|
||||||
|
// If no locale statuses are set, set it and set all others to draft
|
||||||
|
if (!existing) {
|
||||||
|
localeStatus = {
|
||||||
|
...Object.fromEntries(
|
||||||
|
allLocales.filter((code) => code !== incomingLocale).map((code) => [code, 'draft']),
|
||||||
|
),
|
||||||
|
[incomingLocale]: status,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If locales already exist, update the status for the incoming locale
|
||||||
|
const { [incomingLocale]: _, ...rest } = existing
|
||||||
|
localeStatus = {
|
||||||
|
...rest,
|
||||||
|
[incomingLocale]: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createVersionArgs = {
|
const createVersionArgs = {
|
||||||
autosave: Boolean(autosave),
|
autosave: Boolean(autosave),
|
||||||
collectionSlug: undefined as string | undefined,
|
collectionSlug: undefined as string | undefined,
|
||||||
createdAt: operation === 'restoreVersion' ? versionData.createdAt : now,
|
createdAt: operation === 'restoreVersion' ? versionData.createdAt : now,
|
||||||
globalSlug: undefined as string | undefined,
|
globalSlug: undefined as string | undefined,
|
||||||
|
localeStatus,
|
||||||
parent: collection ? id : undefined,
|
parent: collection ? id : undefined,
|
||||||
publishedLocale: publishSpecificLocale || undefined,
|
publishedLocale: publishSpecificLocale || undefined,
|
||||||
req,
|
req,
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export type SanitizedGlobalVersions = {
|
|||||||
export type TypeWithVersion<T> = {
|
export type TypeWithVersion<T> = {
|
||||||
createdAt: string
|
createdAt: string
|
||||||
id: string
|
id: string
|
||||||
|
localeStatus: Record<string, 'draft' | 'published'>
|
||||||
parent: number | string
|
parent: number | string
|
||||||
publishedLocale?: string
|
publishedLocale?: string
|
||||||
snapshot?: boolean
|
snapshot?: boolean
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const Status: React.FC = () => {
|
|||||||
hasPublishedDoc,
|
hasPublishedDoc,
|
||||||
incrementVersionCount,
|
incrementVersionCount,
|
||||||
isTrashed,
|
isTrashed,
|
||||||
|
savedDocumentData: doc,
|
||||||
setHasPublishedDoc,
|
setHasPublishedDoc,
|
||||||
setMostRecentVersionIsAutosaved,
|
setMostRecentVersionIsAutosaved,
|
||||||
setUnpublishedVersionCount,
|
setUnpublishedVersionCount,
|
||||||
@@ -37,6 +38,7 @@ export const Status: React.FC = () => {
|
|||||||
routes: { api },
|
routes: { api },
|
||||||
serverURL,
|
serverURL,
|
||||||
},
|
},
|
||||||
|
getEntityConfig,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
const { reset: resetForm } = useForm()
|
const { reset: resetForm } = useForm()
|
||||||
@@ -46,16 +48,22 @@ export const Status: React.FC = () => {
|
|||||||
const unPublishModalSlug = `confirm-un-publish-${id}`
|
const unPublishModalSlug = `confirm-un-publish-${id}`
|
||||||
const revertModalSlug = `confirm-revert-${id}`
|
const revertModalSlug = `confirm-revert-${id}`
|
||||||
|
|
||||||
let statusToRender: 'changed' | 'draft' | 'published'
|
let statusToRender: 'changed' | 'draft' | 'published' = 'draft'
|
||||||
|
|
||||||
if (unpublishedVersionCount > 0 && hasPublishedDoc) {
|
const collectionConfig = getEntityConfig({ collectionSlug })
|
||||||
statusToRender = 'changed'
|
const globalConfig = getEntityConfig({ globalSlug })
|
||||||
} else if (!hasPublishedDoc) {
|
|
||||||
statusToRender = 'draft'
|
const docConfig = collectionConfig || globalConfig
|
||||||
} else if (hasPublishedDoc && unpublishedVersionCount <= 0) {
|
const autosaveEnabled =
|
||||||
statusToRender = 'published'
|
typeof docConfig?.versions?.drafts === 'object' ? docConfig.versions.drafts.autosave : false
|
||||||
|
|
||||||
|
if (autosaveEnabled) {
|
||||||
|
if (hasPublishedDoc) {
|
||||||
|
statusToRender = unpublishedVersionCount > 0 ? 'changed' : 'published'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusToRender = doc._status || 'draft'
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayStatusKey = isTrashed
|
const displayStatusKey = isTrashed
|
||||||
? hasPublishedDoc
|
? hasPublishedDoc
|
||||||
? 'previouslyPublished'
|
? 'previouslyPublished'
|
||||||
@@ -190,7 +198,7 @@ export const Status: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
{!isTrashed && canUpdate && statusToRender === 'changed' && (
|
{!isTrashed && canUpdate && statusToRender === 'changed' || statusToRender === 'draft' && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
—
|
—
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -428,6 +428,9 @@ export default buildConfigWithDefaults({
|
|||||||
slug: 'global-text',
|
slug: 'global-text',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
experimental: {
|
||||||
|
localizeStatus: true,
|
||||||
|
},
|
||||||
localization: {
|
localization: {
|
||||||
filterAvailableLocales: ({ locales }) => {
|
filterAvailableLocales: ({ locales }) => {
|
||||||
return locales.filter((locale) => locale.code !== 'xx')
|
return locales.filter((locale) => locale.code !== 'xx')
|
||||||
|
|||||||
@@ -673,6 +673,33 @@ describe('Localization', () => {
|
|||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
await expect(page.locator('#field-title')).toBeEmpty()
|
await expect(page.locator('#field-title')).toBeEmpty()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should show localized status in collection list', async () => {
|
||||||
|
await page.goto(urlPostsWithDrafts.create)
|
||||||
|
const engTitle = 'Eng published'
|
||||||
|
const spanTitle = 'Spanish draft'
|
||||||
|
|
||||||
|
await changeLocale(page, defaultLocale)
|
||||||
|
await fillValues({ title: engTitle })
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
await changeLocale(page, spanishLocale)
|
||||||
|
await fillValues({ title: spanTitle })
|
||||||
|
await saveDocAndAssert(page, '#action-save-draft')
|
||||||
|
|
||||||
|
await page.goto(urlPostsWithDrafts.list)
|
||||||
|
|
||||||
|
const columns = page.getByRole('button', { name: 'Columns' })
|
||||||
|
await columns.click()
|
||||||
|
await page.locator('#_status').click()
|
||||||
|
|
||||||
|
await expect(page.locator('.row-1 .cell-title')).toContainText(spanTitle)
|
||||||
|
await expect(page.locator('.row-1 .cell-_status')).toContainText('Draft')
|
||||||
|
|
||||||
|
await changeLocale(page, defaultLocale)
|
||||||
|
await expect(page.locator('.row-1 .cell-title')).toContainText(engTitle)
|
||||||
|
await expect(page.locator('.row-1 .cell-_status')).toContainText('Published')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not show publish specific locale button when no localized fields exist', async () => {
|
test('should not show publish specific locale button when no localized fields exist', async () => {
|
||||||
|
|||||||
@@ -1053,7 +1053,7 @@ describe('Versions', () => {
|
|||||||
|
|
||||||
await textField.fill('spanish draft')
|
await textField.fill('spanish draft')
|
||||||
await saveDocAndAssert(page, '#action-save-draft')
|
await saveDocAndAssert(page, '#action-save-draft')
|
||||||
await expect(status).toContainText('Changed')
|
await expect(status).toContainText('Draft')
|
||||||
|
|
||||||
await changeLocale(page, 'en')
|
await changeLocale(page, 'en')
|
||||||
await textField.fill('english published')
|
await textField.fill('english published')
|
||||||
@@ -1086,7 +1086,7 @@ describe('Versions', () => {
|
|||||||
|
|
||||||
const publishedDoc = data.docs[0]
|
const publishedDoc = data.docs[0]
|
||||||
|
|
||||||
expect(publishedDoc.text).toStrictEqual({
|
expect(publishedDoc?.text).toStrictEqual({
|
||||||
en: 'english published',
|
en: 'english published',
|
||||||
es: 'spanish published',
|
es: 'spanish published',
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user