Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
25b6080
update version displayed status and version pill status
JarrodMFlesch Nov 20, 2025
e8112a7
update pill logic and form submission params
JarrodMFlesch Nov 20, 2025
6daf95f
Merge branch 'feat/experimental-localize-metadata' into feat/experime…
JarrodMFlesch Nov 21, 2025
48a4010
chore: add full unpublish button
jessrynkar Nov 21, 2025
02c2e7f
fix versions list pills
JarrodMFlesch Nov 21, 2025
3086a6d
Merge remote-tracking branch 'refs/remotes/origin/feat/experimental-l…
JarrodMFlesch Nov 21, 2025
97f2593
chore: add failing version view tests
jessrynkar Nov 21, 2025
49a221e
update e2e tests
JarrodMFlesch Nov 21, 2025
48dd413
Merge branch 'feat/experimental-localize-metadata' into feat/experime…
JarrodMFlesch Nov 21, 2025
cae20f9
fix unpublish, should not pass draft true
JarrodMFlesch Nov 21, 2025
4aa67fe
fix versions list pills
JarrodMFlesch Nov 21, 2025
c465baf
chore: add translations
jessrynkar Nov 24, 2025
b6836d4
fix incorrect version count
JarrodMFlesch Nov 24, 2025
aede05c
add locale arg to version ops
JarrodMFlesch Nov 24, 2025
48b3060
chore: fix version types
jessrynkar Nov 25, 2025
3cdae08
chore: wire up globals with localizeStatus
jessrynkar Nov 25, 2025
cf402ed
chore: only add localizeStatus when localized fields exist
jessrynkar Nov 25, 2025
a14a3be
chore: fix type errors in update operation
jessrynkar Nov 25, 2025
edf316f
chore: fix unpublish and dont return snapshots in version view
jessrynkar Nov 26, 2025
6a1d91a
chore: fix version count
jessrynkar Nov 26, 2025
a8c3c62
Merge remote-tracking branch 'origin/feat/experimental-localize-metad…
jessrynkar Nov 26, 2025
c1a9c1f
chore: remove duplicate prop
jessrynkar Nov 26, 2025
c3d3489
chore: only check for snapshot if localization enabled
jessrynkar Nov 27, 2025
a6ee18c
chore: only check for snapshot if fields are localized
jessrynkar Nov 27, 2025
211e5a3
docs: add localizeStatus docs
jessrynkar Nov 28, 2025
2bc20bd
chore: add tests
jessrynkar Nov 28, 2025
060ca08
chore: update unpublishAll in globals update operation
jessrynkar Nov 28, 2025
a9c9bc1
chore: remove console log
jessrynkar Nov 28, 2025
20e26a5
chore: fix tests
jessrynkar Dec 2, 2025
905fa6b
chore: only query snapshot when drafts and localization enabled
jessrynkar Dec 2, 2025
83f1e70
chore: fix test
jessrynkar Dec 2, 2025
b44e3a8
chore: fix version labels
jessrynkar Dec 2, 2025
00b2cb2
chore: update getVersionLabel
jessrynkar Dec 2, 2025
5686a33
chore: merge with base branch
jessrynkar Dec 3, 2025
1918919
chore: fix file from bad merge
jessrynkar Dec 3, 2025
2be54a7
Merge branch 'feat/experimental-localize-metadata' into feat/experime…
jessrynkar Dec 3, 2025
c17ebec
Merge branch 'feat/experimental-localize-metadata' into feat/experime…
jessrynkar Dec 3, 2025
e82eb79
chore: merge with base branch
jessrynkar Dec 4, 2025
1c9f429
fix(drizzle): update drizzle logic to support localized path query wi…
r1tsuu Dec 4, 2025
34d96f8
Merge branch 'feat/experimental-localize-metadata' into feat/experime…
JarrodMFlesch Dec 4, 2025
6c9128c
Merge branch 'feat/experimental-localize-metadata' into feat/experime…
JarrodMFlesch Dec 4, 2025
5be5566
Merge branch 'feat/experimental-localize-metadata' into feat/experime…
JarrodMFlesch Dec 4, 2025
b98d13a
bring back autosave ui changes
JarrodMFlesch Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/configuration/localization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,39 @@ All field types with a `name` property support the `localized` property—even t
strategy.
</Banner>

## Status Localization

Payload allows you to localize the `status` field for **draft enabled** collections and globals. This lets you manage publication status independently for each locale, ensures the admin UI always shows the status for the selected locale, and unpublish content in a single locale.

To enable this feature, set `versions.drafts.localizeStatus` to `true` in your collection or global config:

```ts
import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
// ...
versions: {
drafts: {
// highlight-start
localizeStatus: true,
// highlight-end
},
},
}
```

When enabled, the `status` field will be stored as an object keyed by locales:

```ts
status: {
en: 'published',
es: 'draft',
de: 'published',
}
```

`localizeStatus` is disabled by default, in which case the `status` field returns a single string (`'draft'` or `'published'`) representing the latest document status across all locales.

## Retrieving Localized Docs

When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be
Expand Down
1 change: 1 addition & 0 deletions docs/versions/drafts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Collections and Globals both support the same options for configuring drafts. Yo
| Draft Option | Description |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](/docs/versions/autosave). |
| `localizeStatus` | Localizes the `_status` field when using [Localization](/docs/configuration/localization). Default is `false`. |
| `schedulePublish` | Allow for editors to schedule publish / unpublish events in the future. [More](#scheduled-publish) |
| `validate` | Set `validate` to `true` to validate draft documents when saved. Default is `false`. |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
text-align: center;
background-color: var(--theme-elevation-100);
border-radius: var(--style-radius-s);
padding: 2px 4px;
}
}
79 changes: 57 additions & 22 deletions packages/next/src/views/Document/getVersions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sanitizeID } from '@payloadcms/ui/shared'
import { sanitizeID, traverseForLocalizedFields } from '@payloadcms/ui/shared'
import {
combineQueries,
extractAccessFromPermission,
Expand Down Expand Up @@ -56,6 +56,11 @@ export const getVersions = async ({

const entityConfig = collectionConfig || globalConfig
const versionsConfig = entityConfig?.versions
const hasLocalizedFields = traverseForLocalizedFields(entityConfig.fields)
const localizedDraftsEnabled =
hasDraftsEnabled(entityConfig) &&
typeof payload.config.localization === 'object' &&
hasLocalizedFields

const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions)

Expand Down Expand Up @@ -128,26 +133,34 @@ export const getVersions = async ({
}

if (hasAutosaveEnabled(collectionConfig)) {
const where: Record<string, any> = {
and: [
{
parent: {
equals: id,
},
},
],
}

if (localizedDraftsEnabled) {
where.and.push({
snapshot: {
not_equals: true,
},
})
}

const mostRecentVersion = await payload.findVersions({
collection: collectionConfig.slug,
depth: 0,
limit: 1,
locale,
select: {
autosave: true,
},
user,
where: combineQueries(
{
and: [
{
parent: {
equals: id,
},
},
],
},
extractAccessFromPermission(docPermissions.readVersions),
),
where: combineQueries(where, extractAccessFromPermission(docPermissions.readVersions)),
})

if (
Expand All @@ -162,6 +175,7 @@ export const getVersions = async ({
if (publishedDoc?.updatedAt) {
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
collection: collectionConfig.slug,
locale,
user,
where: combineQueries(
{
Expand Down Expand Up @@ -189,20 +203,31 @@ export const getVersions = async ({
}
}

const countVersionsWhere: Record<string, any> = {
and: [
{
parent: {
equals: id,
},
},
],
}

if (localizedDraftsEnabled) {
countVersionsWhere.and.push({
snapshot: {
not_equals: true,
},
})
}

;({ totalDocs: versionCount } = await payload.countVersions({
collection: collectionConfig.slug,
depth: 0,
locale,
user,
where: combineQueries(
{
and: [
{
parent: {
equals: id,
},
},
],
},
countVersionsWhere,
extractAccessFromPermission(docPermissions.readVersions),
),
}))
Expand Down Expand Up @@ -233,6 +258,7 @@ export const getVersions = async ({
const mostRecentVersion = await payload.findGlobalVersions({
slug: globalConfig.slug,
limit: 1,
locale,
select: {
autosave: true,
},
Expand All @@ -252,6 +278,7 @@ export const getVersions = async ({
;({ totalDocs: unpublishedVersionCount } = await payload.countGlobalVersions({
depth: 0,
global: globalConfig.slug,
locale,
user,
where: combineQueries(
{
Expand All @@ -277,7 +304,15 @@ export const getVersions = async ({
;({ totalDocs: versionCount } = await payload.countGlobalVersions({
depth: 0,
global: globalConfig.slug,
locale,
user,
where: localizedDraftsEnabled
? {
snapshot: {
not_equals: true,
},
}
: undefined,
}))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client'

import type { TypeWithVersion } from 'payload'

import { Pill, useConfig, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import React from 'react'
Expand All @@ -18,10 +20,7 @@ const renderPill = (label: React.ReactNode, pillStyle: Parameters<typeof Pill>[0
}

export const VersionPillLabel: React.FC<{
currentlyPublishedVersion?: {
id: number | string
updatedAt: string
}
currentlyPublishedVersion?: TypeWithVersion<any>
disableDate?: boolean

doc: {
Expand All @@ -31,7 +30,8 @@ export const VersionPillLabel: React.FC<{
updatedAt?: string
version: {
[key: string]: unknown
_status: string
_status: 'draft' | 'published'
updatedAt: string
}
}
/**
Expand All @@ -45,10 +45,7 @@ export const VersionPillLabel: React.FC<{
*/
labelStyle?: 'pill' | 'text'
labelSuffix?: React.ReactNode
latestDraftVersion?: {
id: number | string
updatedAt: string
}
latestDraftVersion?: TypeWithVersion<any>
}> = ({
currentlyPublishedVersion,
disableDate = false,
Expand Down
61 changes: 42 additions & 19 deletions packages/next/src/views/Version/VersionPillLabel/getVersionLabel.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { TFunction } from '@payloadcms/translations'
import type { Pill } from '@payloadcms/ui'

import { type Pill, useLocale } from '@payloadcms/ui'

type Args = {
currentlyPublishedVersion?: {
id: number | string
publishedLocale?: string
updatedAt: string
version: {
updatedAt: string
}
}
latestDraftVersion?: {
id: number | string
Expand All @@ -13,7 +18,8 @@ type Args = {
t: TFunction
version: {
id: number | string
version: { _status?: string }
publishedLocale?: string
version: { _status?: 'draft' | 'published'; updatedAt: string }
}
}

Expand All @@ -31,32 +37,49 @@ export function getVersionLabel({
name: 'currentDraft' | 'currentlyPublished' | 'draft' | 'previouslyPublished' | 'published'
pillStyle: Parameters<typeof Pill>[0]['pillStyle']
} {
const publishedNewerThanDraft =
currentlyPublishedVersion?.updatedAt > latestDraftVersion?.updatedAt
const { code: currentLocale } = useLocale()
const status = version.version._status

if (status === 'draft') {
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

const isCurrentDraft = version.id === latestDraftVersion?.id

return {
name: isCurrentDraft ? 'currentDraft' : 'draft',
label: isCurrentDraft ? t('version:currentDraft') : t('version:draft'),
pillStyle: 'light',
}
}

const publishedInAnotherLocale =
status === 'published' && version.publishedLocale && currentLocale !== version.publishedLocale

if (publishedInAnotherLocale) {
return {
name: isCurrentlyPublished ? 'currentlyPublished' : 'previouslyPublished',
label: isCurrentlyPublished
? t('version:currentlyPublished')
: t('version:previouslyPublished'),
pillStyle: isCurrentlyPublished ? 'success' : 'light',
name: 'currentDraft',
label: t('version:currentDraft'),
pillStyle: 'light',
}
}

const isCurrentlyPublished =
currentlyPublishedVersion && version.id === currentlyPublishedVersion.id

return {
name: isCurrentlyPublished ? 'currentlyPublished' : 'previouslyPublished',
label: isCurrentlyPublished
? t('version:currentlyPublished')
: t('version:previouslyPublished'),
pillStyle: isCurrentlyPublished ? 'success' : 'light',
}
}
2 changes: 1 addition & 1 deletion packages/next/src/views/Version/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export async function VersionView(props: DocumentViewServerProps) {
collectionSlug,
depth: 0,
globalSlug,
locale: 'all',
locale: req.locale,
overrideAccess: false,
parentID: id,
req,
Expand Down
10 changes: 2 additions & 8 deletions packages/next/src/views/Versions/buildColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,13 @@ export const buildVersionColumns = ({
}: {
collectionConfig?: SanitizedCollectionConfig
CreatedAtCellOverride?: React.ComponentType<CreatedAtCellProps>
currentlyPublishedVersion?: {
id: number | string
updatedAt: string
}
currentlyPublishedVersion?: TypeWithVersion<any>
docID?: number | string
docs: PaginatedDocs<TypeWithVersion<any>>['docs']
globalConfig?: SanitizedGlobalConfig
i18n: I18n
isTrashed?: boolean
latestDraftVersion?: {
id: number | string
updatedAt: string
}
latestDraftVersion?: TypeWithVersion<any>
}): Column[] => {
const entityConfig = collectionConfig || globalConfig

Expand Down
Loading
Loading