From 9ef5c9a1f5dfbd06f4a1f8ab86b56d30c412293f Mon Sep 17 00:00:00 2001 From: Jessica Chowdhury Date: Thu, 17 Jul 2025 16:58:09 +0100 Subject: [PATCH] fix: adds enableStatusLocalization option to reflects current locale status across UI --- docs/configuration/localization.mdx | 13 ++- .../db-mongodb/src/createGlobalVersion.ts | 2 + packages/db-mongodb/src/createVersion.ts | 2 + packages/db-mongodb/src/queryDrafts.ts | 7 ++ packages/drizzle/src/createGlobalVersion.ts | 2 + packages/drizzle/src/createVersion.ts | 2 + .../VersionPillLabel/VersionPillLabel.tsx | 2 +- .../Versions/cells/AutosaveCell/index.tsx | 1 + .../src/collections/operations/create.ts | 1 + .../operations/utilities/update.ts | 1 + packages/payload/src/config/client.ts | 5 + packages/payload/src/config/types.ts | 6 + packages/payload/src/database/types.ts | 2 + .../payload/src/globals/operations/update.ts | 1 + .../src/versions/buildCollectionFields.ts | 29 +++++ .../payload/src/versions/buildGlobalFields.ts | 29 +++++ packages/payload/src/versions/saveVersion.ts | 103 +++++++++++++----- packages/payload/src/versions/types.ts | 1 + packages/ui/src/elements/Status/index.tsx | 16 ++- test/localization/config.ts | 1 + test/localization/e2e.spec.ts | 22 ++++ 21 files changed, 211 insertions(+), 37 deletions(-) diff --git a/docs/configuration/localization.mdx b/docs/configuration/localization.mdx index 9174804a7c..da0bef94e9 100644 --- a/docs/configuration/localization.mdx +++ b/docs/configuration/localization.mdx @@ -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 diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index c5910231e6..20975ba224 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -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, diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index ec733baa83..d2849d2725 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -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, diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index c43e0c52f4..889dae507a 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -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 } diff --git a/packages/drizzle/src/createGlobalVersion.ts b/packages/drizzle/src/createGlobalVersion.ts index 5ac12fa8c2..bb33fbe6d0 100644 --- a/packages/drizzle/src/createGlobalVersion.ts +++ b/packages/drizzle/src/createGlobalVersion.ts @@ -15,6 +15,7 @@ export async function createGlobalVersion( autosave, createdAt, globalSlug, + localeStatus, publishedLocale, req, returning, @@ -35,6 +36,7 @@ export async function createGlobalVersion( autosave, createdAt, latest: true, + localeStatus, publishedLocale, snapshot, updatedAt, diff --git a/packages/drizzle/src/createVersion.ts b/packages/drizzle/src/createVersion.ts index cb612b85b4..12c41dd0c7 100644 --- a/packages/drizzle/src/createVersion.ts +++ b/packages/drizzle/src/createVersion.ts @@ -15,6 +15,7 @@ export async function createVersion( autosave, collectionSlug, createdAt, + localeStatus, parent, publishedLocale, req, @@ -40,6 +41,7 @@ export async function createVersion( autosave, createdAt, latest: true, + localeStatus, parent, publishedLocale, snapshot, diff --git a/packages/next/src/views/Version/VersionPillLabel/VersionPillLabel.tsx b/packages/next/src/views/Version/VersionPillLabel/VersionPillLabel.tsx index 1ca29a7f43..45a5cde220 100644 --- a/packages/next/src/views/Version/VersionPillLabel/VersionPillLabel.tsx +++ b/packages/next/src/views/Version/VersionPillLabel/VersionPillLabel.tsx @@ -116,7 +116,7 @@ export const VersionPillLabel: React.FC<{ )} )} - {localeLabel && {localeLabel}} + {localeLabel && {localeLabel}} ) } diff --git a/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx b/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx index ee18767109..8d17f30f49 100644 --- a/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx +++ b/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx @@ -19,6 +19,7 @@ type AutosaveCellProps = { rowData: { autosave?: boolean id: number | string + localeStatus?: Record publishedLocale?: string version: { _status: string diff --git a/packages/payload/src/collections/operations/create.ts b/packages/payload/src/collections/operations/create.ts index cc09c1ad6a..6c0bd40562 100644 --- a/packages/payload/src/collections/operations/create.ts +++ b/packages/payload/src/collections/operations/create.ts @@ -285,6 +285,7 @@ export const createOperation = async < autosave, collection: collectionConfig, docWithLocales: result, + locale, payload, req, }) diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts index 01cd589427..9c46773d49 100644 --- a/packages/payload/src/collections/operations/utilities/update.ts +++ b/packages/payload/src/collections/operations/utilities/update.ts @@ -314,6 +314,7 @@ export const updateDocument = async < collection: collectionConfig, docWithLocales: result, draft: shouldSaveDraft, + locale, payload, publishSpecificLocale, req, diff --git a/packages/payload/src/config/client.ts b/packages/payload/src/config/client.ts index f6fa1266d9..efad902a0a 100644 --- a/packages/payload/src/config/client.ts +++ b/packages/payload/src/config/client.ts @@ -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 } diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index fdd2e460f6..8d5a49cb53 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -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. diff --git a/packages/payload/src/database/types.ts b/packages/payload/src/database/types.ts index ca94f76997..c773c896b7 100644 --- a/packages/payload/src/database/types.ts +++ b/packages/payload/src/database/types.ts @@ -386,6 +386,7 @@ export type CreateVersionArgs = { autosave: boolean collectionSlug: CollectionSlug createdAt: string + localeStatus: Record /** 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 = { autosave: boolean createdAt: string globalSlug: GlobalSlug + localeStatus: Record /** ID of the parent document for which the version should be created for */ parent: number | string publishedLocale?: string diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index 0f3f3869f7..d72d24da0f 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -282,6 +282,7 @@ export const updateOperation = async < docWithLocales: result, draft: shouldSaveDraft, global: globalConfig, + locale, payload, publishSpecificLocale, req, diff --git a/packages/payload/src/versions/buildCollectionFields.ts b/packages/payload/src/versions/buildCollectionFields.ts index 0896591896..03d8c70edd 100644 --- a/packages/payload/src/versions/buildCollectionFields.ts +++ b/packages/payload/src/versions/buildCollectionFields.ts @@ -62,6 +62,35 @@ export const buildVersionCollectionFields = ( 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({ diff --git a/packages/payload/src/versions/buildGlobalFields.ts b/packages/payload/src/versions/buildGlobalFields.ts index 06c1a067cd..f68b807baa 100644 --- a/packages/payload/src/versions/buildGlobalFields.ts +++ b/packages/payload/src/versions/buildGlobalFields.ts @@ -56,6 +56,35 @@ export const buildVersionGlobalFields = ( 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({ diff --git a/packages/payload/src/versions/saveVersion.ts b/packages/payload/src/versions/saveVersion.ts index 43df69ede8..974572e315 100644 --- a/packages/payload/src/versions/saveVersion.ts +++ b/packages/payload/src/versions/saveVersion.ts @@ -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, diff --git a/packages/payload/src/versions/types.ts b/packages/payload/src/versions/types.ts index efbc4e3c95..d79c59d533 100644 --- a/packages/payload/src/versions/types.ts +++ b/packages/payload/src/versions/types.ts @@ -122,6 +122,7 @@ export type SanitizedGlobalVersions = { export type TypeWithVersion = { createdAt: string id: string + localeStatus: Record parent: number | string publishedLocale?: string snapshot?: boolean diff --git a/packages/ui/src/elements/Status/index.tsx b/packages/ui/src/elements/Status/index.tsx index cd7130e2cd..dfac2eb72c 100644 --- a/packages/ui/src/elements/Status/index.tsx +++ b/packages/ui/src/elements/Status/index.tsx @@ -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 = () => { /> )} - {canUpdate && statusToRender === 'changed' && ( + {canUpdate && (statusToRender === 'draft' || statusToRender === 'changed') && (  —