fix: adds enableStatusLocalization option to reflects current locale status across UI

This commit is contained in:
Jessica Chowdhury
2025-07-17 16:58:09 +01:00
parent be8e8d9c7f
commit 9ef5c9a1f5
21 changed files with 211 additions and 37 deletions

View File

@@ -78,12 +78,13 @@ export default buildConfig({
The following options are available:
| Option | Description |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
| **`filterAvailableLocales`** | A function that is called with the array of `locales` and the `req`, it should return locales to show in admin UI selector. [See more](#filter-available-options). |
| Option | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
| **`enableStatusLocalization`** | **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`. |
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
| **`filterAvailableLocales`** | A function that is called with the array of `locales` and the `req`, it should return locales to show in admin UI selector. [See more](#filter-available-options). |
### Locales

View File

@@ -12,6 +12,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
autosave,
createdAt,
globalSlug,
localeStatus,
parent,
publishedLocale,
req,
@@ -31,6 +32,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
autosave,
createdAt,
latest: true,
localeStatus,
parent,
publishedLocale,
snapshot,

View File

@@ -12,6 +12,7 @@ export const createVersion: CreateVersion = async function createVersion(
autosave,
collectionSlug,
createdAt,
localeStatus,
parent,
publishedLocale,
req,
@@ -35,6 +36,7 @@ export const createVersion: CreateVersion = async function createVersion(
autosave,
createdAt,
latest: true,
localeStatus,
parent,
publishedLocale,
snapshot,

View File

@@ -167,6 +167,13 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
for (let i = 0; i < result.docs.length; i++) {
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].id = id
}

View File

@@ -15,6 +15,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
autosave,
createdAt,
globalSlug,
localeStatus,
publishedLocale,
req,
returning,
@@ -35,6 +36,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
autosave,
createdAt,
latest: true,
localeStatus,
publishedLocale,
snapshot,
updatedAt,

View File

@@ -15,6 +15,7 @@ export async function createVersion<T extends TypeWithID>(
autosave,
collectionSlug,
createdAt,
localeStatus,
parent,
publishedLocale,
req,
@@ -40,6 +41,7 @@ export async function createVersion<T extends TypeWithID>(
autosave,
createdAt,
latest: true,
localeStatus,
parent,
publishedLocale,
snapshot,

View File

@@ -116,7 +116,7 @@ export const VersionPillLabel: React.FC<{
)}
</React.Fragment>
)}
{localeLabel && <Pill>{localeLabel}</Pill>}
{localeLabel && <Pill size="small">{localeLabel}</Pill>}
</div>
)
}

View File

@@ -19,6 +19,7 @@ type AutosaveCellProps = {
rowData: {
autosave?: boolean
id: number | string
localeStatus?: Record<string, 'draft' | 'published'>
publishedLocale?: string
version: {
_status: string

View File

@@ -285,6 +285,7 @@ export const createOperation = async <
autosave,
collection: collectionConfig,
docWithLocales: result,
locale,
payload,
req,
})

View File

@@ -314,6 +314,7 @@ export const updateDocument = async <
collection: collectionConfig,
docWithLocales: result,
draft: shouldSaveDraft,
locale,
payload,
publishSpecificLocale,
req,

View File

@@ -193,6 +193,11 @@ export const createClientConfig = ({
config.localization.defaultLocalePublishOption
}
if (config.localization.enableStatusLocalization) {
clientConfig.localization.enableStatusLocalization =
config.localization.enableStatusLocalization
}
if (config.localization.fallback) {
clientConfig.localization.fallback = config.localization.fallback
}

View File

@@ -486,6 +486,12 @@ export type BaseLocalizationConfig = {
* @default 'all'
*/
defaultLocalePublishOption?: 'active' | 'all'
/**
* Enable localization of the status of the document.
* If enabled, the status will reflect the current locale throughout the Admin UI.
* @default false
*/
enableStatusLocalization?: boolean
/** Set to `true` to let missing values in localised fields fall back to the values in `defaultLocale`
*
* If false, then no requests will fallback unless a fallbackLocale is specified in the request.

View File

@@ -386,6 +386,7 @@ export type CreateVersionArgs<T = TypeWithID> = {
autosave: boolean
collectionSlug: CollectionSlug
createdAt: string
localeStatus: Record<string, 'draft' | 'published'>
/** ID of the parent document for which the version should be created for */
parent: number | string
publishedLocale?: string
@@ -410,6 +411,7 @@ export type CreateGlobalVersionArgs<T = TypeWithID> = {
autosave: boolean
createdAt: string
globalSlug: GlobalSlug
localeStatus: Record<string, 'draft' | 'published'>
/** ID of the parent document for which the version should be created for */
parent: number | string
publishedLocale?: string

View File

@@ -282,6 +282,7 @@ export const updateOperation = async <
docWithLocales: result,
draft: shouldSaveDraft,
global: globalConfig,
locale,
payload,
publishSpecificLocale,
req,

View File

@@ -62,6 +62,35 @@ export const buildVersionCollectionFields = <T extends boolean = false>(
return locale.code
}),
})
if (config.localization.enableStatusLocalization) {
const localeStatusFields: Field[] = config.localization.locales.map((locale) => {
const code = typeof locale === 'string' ? locale : locale.code
return {
name: code,
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
}
})
fields.push({
name: 'localeStatus',
type: 'group',
admin: {
disableBulkEdit: true,
disabled: true,
},
fields: localeStatusFields,
index: true,
...(flatten && {
flattenedFields: localeStatusFields as FlattenedField[],
})!,
})
}
}
fields.push({

View File

@@ -56,6 +56,35 @@ export const buildVersionGlobalFields = <T extends boolean = false>(
return locale.code
}),
})
if (config.localization.enableStatusLocalization) {
const localeStatusFields: Field[] = config.localization.locales.map((locale) => {
const code = typeof locale === 'string' ? locale : locale.code
return {
name: code,
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
}
})
fields.push({
name: 'localeStatus',
type: 'group',
admin: {
disableBulkEdit: true,
disabled: true,
},
fields: localeStatusFields,
index: true,
...(flatten && {
flattenedFields: localeStatusFields as FlattenedField[],
})!,
})
}
}
fields.push({

View File

@@ -1,3 +1,5 @@
import { version } from 'os'
// @ts-strict-ignore
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
@@ -16,6 +18,7 @@ type Args = {
draft?: boolean
global?: SanitizedGlobalConfig
id?: number | string
locale?: null | string
payload: Payload
publishSpecificLocale?: string
req?: PayloadRequest
@@ -30,6 +33,7 @@ export const saveVersion = async ({
docWithLocales: doc,
draft,
global,
locale,
payload,
publishSpecificLocale,
req,
@@ -40,6 +44,7 @@ export const saveVersion = async ({
let createNewVersion = true
const now = new Date().toISOString()
const versionData = deepCopyObjectSimple(doc)
if (draft) {
versionData._status = 'draft'
}
@@ -53,39 +58,39 @@ export const saveVersion = async ({
}
try {
if (autosave) {
let docs
const findVersionArgs = {
let docs
const findVersionArgs = {
limit: 1,
pagination: false,
req,
sort: '-updatedAt',
}
if (collection) {
;({ docs } = await payload.db.findVersions({
...findVersionArgs,
collection: collection.slug,
limit: 1,
pagination: false,
req,
sort: '-updatedAt',
}
if (collection) {
;({ docs } = await payload.db.findVersions({
...findVersionArgs,
collection: collection.slug,
limit: 1,
pagination: false,
req,
where: {
parent: {
equals: id,
},
where: {
parent: {
equals: id,
},
}))
} else {
;({ docs } = await payload.db.findGlobalVersions({
...findVersionArgs,
global: global!.slug,
limit: 1,
pagination: false,
req,
}))
}
const [latestVersion] = docs
},
}))
} else {
;({ docs } = await payload.db.findGlobalVersions({
...findVersionArgs,
global: global!.slug,
limit: 1,
pagination: false,
req,
}))
}
const [latestVersion] = docs
if (autosave) {
// overwrite the latest version if it's set to autosave
if (latestVersion && 'autosave' in latestVersion && latestVersion.autosave === true) {
createNewVersion = false
@@ -123,11 +128,53 @@ export const saveVersion = async ({
}
if (createNewVersion) {
let localeStatus = {}
const localizationEnabled =
payload.config.localization && payload.config.localization.locales.length > 0
if (
localizationEnabled &&
payload.config.localization !== false &&
payload.config.localization.enableStatusLocalization
) {
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 = {
autosave: Boolean(autosave),
collectionSlug: undefined as string | undefined,
createdAt: now,
globalSlug: undefined as string | undefined,
localeStatus,
parent: collection ? id : undefined,
publishedLocale: publishSpecificLocale || undefined,
req,

View File

@@ -122,6 +122,7 @@ export type SanitizedGlobalVersions = {
export type TypeWithVersion<T> = {
createdAt: string
id: string
localeStatus: Record<string, 'draft' | 'published'>
parent: number | string
publishedLocale?: string
snapshot?: boolean

View File

@@ -36,6 +36,7 @@ export const Status: React.FC = () => {
routes: { api },
serverURL,
},
getEntityConfig,
} = useConfig()
const { reset: resetForm } = useForm()
@@ -47,8 +48,19 @@ export const Status: React.FC = () => {
let statusToRender: 'changed' | 'draft' | 'published'
const collectionConfig = getEntityConfig({ collectionSlug })
const globalConfig = getEntityConfig({ globalSlug })
const docConfig = collectionConfig || globalConfig
const autosaveEnabled =
typeof docConfig?.versions?.drafts === 'object' ? docConfig.versions.drafts.autosave : false
if (unpublishedVersionCount > 0 && hasPublishedDoc) {
statusToRender = 'changed'
if (autosaveEnabled) {
statusToRender = 'changed'
} else {
statusToRender = 'draft'
}
} else if (!hasPublishedDoc) {
statusToRender = 'draft'
} else if (hasPublishedDoc && unpublishedVersionCount <= 0) {
@@ -183,7 +195,7 @@ export const Status: React.FC = () => {
/>
</React.Fragment>
)}
{canUpdate && statusToRender === 'changed' && (
{canUpdate && (statusToRender === 'draft' || statusToRender === 'changed') && (
<React.Fragment>
&nbsp;&mdash;&nbsp;
<Button

View File

@@ -432,6 +432,7 @@ export default buildConfigWithDefaults({
},
defaultLocale,
fallback: true,
enableStatusLocalization: true,
locales: [
{
code: 'xx',

View File

@@ -618,6 +618,28 @@ describe('Localization', () => {
await expect(searchInput).toBeVisible()
await expect(searchInput).toHaveAttribute('placeholder', 'Search by Full title')
})
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)
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')
})
})
async function fillValues(data: Partial<LocalizedPost>) {