From 8b255f6d0ce73ca43f21a0f87f22af3b2741b67c Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 11:06:56 -0400 Subject: [PATCH 01/16] test: add localized status shared draft coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 0194bddb24e0613697f75d404b8a92ed88224e6f) --- test/localization/int.spec.ts | 197 ++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 487ec51e490..204fbcbb031 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -4077,6 +4077,203 @@ describe('Localization', () => { expect(latestVersionDoc.text!.es).toBe('spanish draft 2') }) + it('should mark all locales as draft when a draft save changes a non-localized field', async () => { + const doc = await payload.create({ + collection: allFieldsLocalizedSlug, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared published', + }, + text: 'english published', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + text: 'spanish published', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared draft', + }, + text: 'english draft', + _status: 'draft', + }, + draft: true, + locale: defaultLocale, + }) + + const allLocalesDraft = await payload.findByID({ + collection: allFieldsLocalizedSlug, + id: doc.id, + draft: true, + locale: 'all', + }) + + expect(allLocalesDraft._status!.en).toBe('draft') + expect(allLocalesDraft._status!.es).toBe('draft') + expect(allLocalesDraft.text!.en).toBe('english draft') + expect(allLocalesDraft.text!.es).toBe('spanish published') + expect(allLocalesDraft.nonLocalizedGroup?.nonLocalizedText).toBe('shared draft') + + const spanishView = await payload.findByID({ + collection: allFieldsLocalizedSlug, + id: doc.id, + draft: true, + locale: spanishLocale, + }) + + expect(spanishView._status).toBe('draft') + expect(spanishView.text).toBe('spanish published') + expect(spanishView.nonLocalizedGroup?.nonLocalizedText).toBe('shared draft') + }) + + it('should keep published locales published when a draft save changes only localized fields', async () => { + const doc = await payload.create({ + collection: allFieldsLocalizedSlug, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared published localized-only test', + }, + text: 'english published localized-only test', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + text: 'spanish published localized-only test', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + text: 'english draft localized-only test', + _status: 'draft', + }, + draft: true, + locale: defaultLocale, + }) + + const allLocalesDraft = await payload.findByID({ + collection: allFieldsLocalizedSlug, + id: doc.id, + draft: true, + locale: 'all', + }) + + expect(allLocalesDraft._status!.en).toBe('draft') + expect(allLocalesDraft._status!.es).toBe('published') + expect(allLocalesDraft.text!.en).toBe('english draft localized-only test') + expect(allLocalesDraft.text!.es).toBe('spanish published localized-only test') + expect(allLocalesDraft.nonLocalizedGroup?.nonLocalizedText).toBe( + 'shared published localized-only test', + ) + + const spanishDraftDoc = await payload.create({ + collection: allFieldsLocalizedSlug, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'inverse shared published', + }, + text: 'inverse english published', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: spanishDraftDoc.id, + data: { + text: 'inverse spanish published', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: spanishDraftDoc.id, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'inverse shared draft', + }, + text: 'inverse spanish draft', + _status: 'draft', + }, + draft: true, + locale: spanishLocale, + }) + + const englishView = await payload.findByID({ + collection: allFieldsLocalizedSlug, + id: spanishDraftDoc.id, + draft: true, + locale: defaultLocale, + }) + + expect(englishView._status).toBe('published') + expect(englishView.text).toBe('inverse english published') + expect(englishView.nonLocalizedGroup?.nonLocalizedText).toBe('inverse shared draft') + }) + + it('should resolve drafts by localized status when no locale is specified', async () => { + const doc = await payload.create({ + collection: allFieldsLocalizedSlug, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared published without locale', + }, + text: 'published without locale', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared draft without locale', + }, + text: 'draft without locale', + _status: 'draft', + }, + draft: true, + locale: defaultLocale, + }) + + const response = await restClient.GET(`/${allFieldsLocalizedSlug}/${doc.id}?draft=true`) + + expect(response.status).toBe(200) + + const result = await response.json() + + expect(result._status).toBe('draft') + expect(result.text).toBe('draft without locale') + expect(result.nonLocalizedGroup?.nonLocalizedText).toBe('shared draft without locale') + }) + it('should allow querying metadata per locale', async () => { const doc = await payload.create({ collection: allFieldsLocalizedSlug, From 9a2cc58cb5b2bab586582dfd52e22a72681e6b00 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 11:09:54 -0400 Subject: [PATCH 02/16] test: fix localized-only draft status coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit eb02cb519447f739bd4c93a7fb63cba8b14ad819) --- test/localization/int.spec.ts | 47 ----------------------------------- 1 file changed, 47 deletions(-) diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 204fbcbb031..55bd42fb0ec 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -4187,53 +4187,6 @@ describe('Localization', () => { expect(allLocalesDraft.nonLocalizedGroup?.nonLocalizedText).toBe( 'shared published localized-only test', ) - - const spanishDraftDoc = await payload.create({ - collection: allFieldsLocalizedSlug, - data: { - nonLocalizedGroup: { - nonLocalizedText: 'inverse shared published', - }, - text: 'inverse english published', - _status: 'published', - }, - locale: defaultLocale, - }) - - await payload.update({ - collection: allFieldsLocalizedSlug, - id: spanishDraftDoc.id, - data: { - text: 'inverse spanish published', - _status: 'published', - }, - locale: spanishLocale, - }) - - await payload.update({ - collection: allFieldsLocalizedSlug, - id: spanishDraftDoc.id, - data: { - nonLocalizedGroup: { - nonLocalizedText: 'inverse shared draft', - }, - text: 'inverse spanish draft', - _status: 'draft', - }, - draft: true, - locale: spanishLocale, - }) - - const englishView = await payload.findByID({ - collection: allFieldsLocalizedSlug, - id: spanishDraftDoc.id, - draft: true, - locale: defaultLocale, - }) - - expect(englishView._status).toBe('published') - expect(englishView.text).toBe('inverse english published') - expect(englishView.nonLocalizedGroup?.nonLocalizedText).toBe('inverse shared draft') }) it('should resolve drafts by localized status when no locale is specified', async () => { From 7a7c4252f5d57f1013e7589c4478ec8e833b6794 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 11:22:03 -0400 Subject: [PATCH 03/16] feat: detect shared draft field changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 50981bb3abe622efc19da10f026444b9d9d02d4a) --- .../drafts/hasNonLocalizedDataChanged.spec.ts | 255 ++++++++++++++++++ .../drafts/hasNonLocalizedDataChanged.ts | 246 +++++++++++++++++ 2 files changed, 501 insertions(+) create mode 100644 packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts create mode 100644 packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts diff --git a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts new file mode 100644 index 00000000000..6d2c90b933c --- /dev/null +++ b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts @@ -0,0 +1,255 @@ +import { describe, expect, it } from 'vitest' + +import type { Field } from '../../fields/config/types.js' + +import { hasNonLocalizedDataChanged } from './hasNonLocalizedDataChanged.js' + +const fields: Field[] = [ + { + name: 'title', + type: 'text', + localized: true, + }, + { + name: 'summary', + type: 'text', + }, + { + name: 'content', + type: 'richText', + }, + { + name: 'meta', + type: 'group', + fields: [ + { + name: 'localizedDescription', + type: 'text', + localized: true, + }, + { + name: 'sharedDescription', + type: 'text', + }, + ], + }, + { + name: 'rows', + type: 'array', + fields: [ + { + name: 'localizedLabel', + type: 'text', + localized: true, + }, + { + name: 'sharedLabel', + type: 'text', + }, + ], + }, + { + name: 'blocks', + type: 'blocks', + blocks: [ + { + slug: 'textBlock', + fields: [ + { + name: 'localizedBlockText', + type: 'text', + localized: true, + }, + { + name: 'sharedBlockText', + type: 'text', + }, + ], + }, + ], + }, +] + +describe('hasNonLocalizedDataChanged', () => { + it('should ignore localized field changes', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + title: { en: 'Draft EN', es: 'Published ES' }, + summary: 'Shared', + }, + before: { + title: { en: 'Published EN', es: 'Published ES' }, + summary: 'Shared', + }, + configBlockReferences: [], + fields, + }), + ).toBe(false) + }) + + it('should detect non-localized scalar field changes', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + summary: 'Shared draft', + }, + before: { + summary: 'Shared published', + }, + configBlockReferences: [], + fields, + }), + ).toBe(true) + }) + + it('should detect rich-text-like non-localized object changes', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + content: { + root: { + children: [{ text: 'Draft rich text', type: 'text' }], + type: 'root', + }, + }, + }, + before: { + content: { + root: { + children: [{ text: 'Published rich text', type: 'text' }], + type: 'root', + }, + }, + }, + configBlockReferences: [], + fields, + }), + ).toBe(true) + }) + + it('should compare non-localized children inside non-localized groups', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + meta: { + localizedDescription: { en: 'Draft EN' }, + sharedDescription: 'Shared draft', + }, + }, + before: { + meta: { + localizedDescription: { en: 'Published EN' }, + sharedDescription: 'Shared published', + }, + }, + configBlockReferences: [], + fields, + }), + ).toBe(true) + }) + + it('should ignore localized children inside non-localized groups', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + meta: { + localizedDescription: { en: 'Draft EN' }, + sharedDescription: 'Shared', + }, + }, + before: { + meta: { + localizedDescription: { en: 'Published EN' }, + sharedDescription: 'Shared', + }, + }, + configBlockReferences: [], + fields, + }), + ).toBe(false) + }) + + it('should detect non-localized array structure changes', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + rows: [ + { id: 'row-1', sharedLabel: 'One' }, + { id: 'row-2', sharedLabel: 'Two' }, + ], + }, + before: { + rows: [{ id: 'row-1', sharedLabel: 'One' }], + }, + configBlockReferences: [], + fields, + }), + ).toBe(true) + }) + + it('should ignore localized child changes inside existing non-localized array rows', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + rows: [{ id: 'row-1', localizedLabel: { en: 'Draft EN' }, sharedLabel: 'One' }], + }, + before: { + rows: [{ id: 'row-1', localizedLabel: { en: 'Published EN' }, sharedLabel: 'One' }], + }, + configBlockReferences: [], + fields, + }), + ).toBe(false) + }) + + it('should detect non-localized block structure changes', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + blocks: [ + { + blockType: 'textBlock', + id: 'block-1', + sharedBlockText: 'One', + }, + ], + }, + before: { + blocks: [], + }, + configBlockReferences: [], + fields, + }), + ).toBe(true) + }) + + it('should ignore localized child changes inside existing non-localized blocks', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + blocks: [ + { + blockType: 'textBlock', + id: 'block-1', + localizedBlockText: { en: 'Draft EN' }, + sharedBlockText: 'Shared', + }, + ], + }, + before: { + blocks: [ + { + blockType: 'textBlock', + id: 'block-1', + localizedBlockText: { en: 'Published EN' }, + sharedBlockText: 'Shared', + }, + ], + }, + configBlockReferences: [], + fields, + }), + ).toBe(false) + }) +}) diff --git a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts new file mode 100644 index 00000000000..9309bc03e25 --- /dev/null +++ b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts @@ -0,0 +1,246 @@ +import type { Block, Field, FlattenedBlock } from '../../fields/config/types.js' +import type { SanitizedConfig } from '../../index.js' +import type { JsonObject } from '../../types/index.js' + +import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../fields/config/types.js' + +type ComparableValue = + | { [key: string]: ComparableValue } + | boolean + | ComparableValue[] + | null + | number + | string + +type HasNonLocalizedDataChangedArgs = { + after: JsonObject + before: JsonObject + configBlockReferences: SanitizedConfig['blocks'] + fields: Field[] +} + +type NormalizeArgs = { + configBlockReferences: SanitizedConfig['blocks'] + data: JsonObject + fields: Field[] + parentIsLocalized?: boolean +} + +export const hasNonLocalizedDataChanged = ({ + after, + before, + configBlockReferences, + fields, +}: HasNonLocalizedDataChangedArgs): boolean => { + const beforeSharedData = normalizeNonLocalizedData({ + configBlockReferences, + data: before, + fields, + }) + const afterSharedData = normalizeNonLocalizedData({ + configBlockReferences, + data: after, + fields, + }) + + return stableStringify(beforeSharedData) !== stableStringify(afterSharedData) +} + +const normalizeNonLocalizedData = ({ + configBlockReferences, + data, + fields, + parentIsLocalized = false, +}: NormalizeArgs): Record => { + const result: Record = {} + + for (const field of fields) { + if (fieldAffectsData(field)) { + if (!(field.name in data)) { + continue + } + + const fieldIsLocalized = fieldShouldBeLocalized({ field, parentIsLocalized }) + + if (fieldIsLocalized) { + continue + } + + const value = data[field.name] + + switch (field.type) { + case 'array': { + result[field.name] = normalizeArrayRows({ + configBlockReferences, + fields: field.fields, + rows: Array.isArray(value) ? value : [], + }) + break + } + + case 'blocks': { + result[field.name] = normalizeBlocks({ + blocks: Array.isArray(value) ? value : [], + configBlockReferences, + fieldBlocks: field.blocks, + }) + break + } + + case 'group': { + result[field.name] = normalizeNonLocalizedData({ + configBlockReferences, + data: isJsonObject(value) ? value : {}, + fields: field.fields, + parentIsLocalized, + }) + break + } + + default: { + result[field.name] = normalizeComparableValue(value) + break + } + } + } else if ('fields' in field && Array.isArray(field.fields)) { + Object.assign( + result, + normalizeNonLocalizedData({ + configBlockReferences, + data, + fields: field.fields, + parentIsLocalized, + }), + ) + } else if (field.type === 'tabs') { + for (const tab of field.tabs) { + const tabIsLocalized = fieldShouldBeLocalized({ field: tab, parentIsLocalized }) + + if (tabHasName(tab)) { + if (tabIsLocalized || !(tab.name in data)) { + continue + } + + const tabValue = data[tab.name] + result[tab.name] = normalizeNonLocalizedData({ + configBlockReferences, + data: isJsonObject(tabValue) ? tabValue : {}, + fields: tab.fields, + parentIsLocalized, + }) + } else { + Object.assign( + result, + normalizeNonLocalizedData({ + configBlockReferences, + data, + fields: tab.fields, + parentIsLocalized, + }), + ) + } + } + } + } + + return result +} + +const normalizeArrayRows = ({ + configBlockReferences, + fields, + rows, +}: { + configBlockReferences: SanitizedConfig['blocks'] + fields: Field[] + rows: unknown[] +}): ComparableValue[] => { + return rows.map((row) => { + const rowData = isJsonObject(row) ? row : {} + + return { + id: normalizeComparableValue(rowData.id), + ...normalizeNonLocalizedData({ + configBlockReferences, + data: rowData, + fields, + }), + } + }) +} + +const normalizeBlocks = ({ + blocks, + configBlockReferences, + fieldBlocks, +}: { + blocks: unknown[] + configBlockReferences: SanitizedConfig['blocks'] + fieldBlocks: (Block | string)[] +}): ComparableValue[] => { + return blocks.map((blockData) => { + const blockObject = isJsonObject(blockData) ? blockData : {} + const blockType = typeof blockObject.blockType === 'string' ? blockObject.blockType : undefined + const blockOrSlug = fieldBlocks.find((block) => { + const slug = typeof block === 'string' ? block : block.slug + return slug === blockType + }) + const block: Block | FlattenedBlock | undefined = + typeof blockOrSlug === 'string' + ? configBlockReferences?.find((blockRef) => blockRef.slug === blockOrSlug) + : blockOrSlug + + return { + id: normalizeComparableValue(blockObject.id), + blockName: normalizeComparableValue(blockObject.blockName), + blockType: normalizeComparableValue(blockObject.blockType), + ...(block + ? normalizeNonLocalizedData({ + configBlockReferences, + data: blockObject, + fields: block.fields, + }) + : {}), + } + }) +} + +const isJsonObject = (value: unknown): value is JsonObject => { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)) +} + +const normalizeComparableValue = (value: unknown): ComparableValue => { + if (Array.isArray(value)) { + return value.map((item) => normalizeComparableValue(item)) + } + + if (value && typeof value === 'object') { + const objectValue = value as Record + const result: Record = {} + + for (const key of Object.keys(objectValue).sort()) { + const normalizedValue = normalizeComparableValue(objectValue[key]) + + if (typeof normalizedValue !== 'undefined') { + result[key] = normalizedValue + } + } + + return result + } + + if ( + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'string' || + value === null + ) { + return value + } + + return null +} + +const stableStringify = (value: ComparableValue | Record): string => { + return JSON.stringify(value) +} From 847e281e60e53a321e943dbd1e527243d983e08a Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 12:40:51 -0400 Subject: [PATCH 04/16] fix: draft all locales for shared collection draft changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 6da841638fab7d03686d966aa9f14a93d7bd78ea) --- .../operations/utilities/update.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts index 8925bad8a8b..553487df002 100644 --- a/packages/payload/src/collections/operations/utilities/update.ts +++ b/packages/payload/src/collections/operations/utilities/update.ts @@ -38,6 +38,32 @@ import { hasLocalizeStatusEnabled, } from '../../../utilities/getVersionsConfig.js' import { mergeLocalizedData } from '../../../utilities/mergeLocalizedData.js' +import { buildLocalizedPublishData } from '../../../versions/buildSingleLocalePublishData.js' +import { hasNonLocalizedDataChanged } from '../../../versions/drafts/hasNonLocalizedDataChanged.js' + +const removeUndefinedValues = (value: T): T => { + if (Array.isArray(value)) { + return value + .map((item) => removeUndefinedValues(item)) + .filter((item) => typeof item !== 'undefined') as T + } + + if (value && typeof value === 'object') { + const cleanedValue: Record = {} + + for (const [key, item] of Object.entries(value as Record)) { + const cleanedItem = removeUndefinedValues(item) + + if (typeof cleanedItem !== 'undefined') { + cleanedValue[key] = cleanedItem + } + } + + return cleanedValue as T + } + + return value +} export type SharedUpdateDocumentArgs = { autosave: boolean collectionConfig: SanitizedCollectionConfig @@ -278,6 +304,59 @@ export const updateDocument = async < let result: JsonObject = await beforeChange(beforeChangeArgs) let snapshotToSave: JsonObject | undefined + if ( + config.localization && + hasLocalizeStatusEnabled(collectionConfig) && + typeof result._status === 'string' + ) { + const statusStr = result._status + result._status = {} + for (const localeCode of config.localization.localeCodes) { + ;(result._status as Record)[localeCode] = statusStr + } + } + + const shouldDraftAllLocales = + isSavingDraft && + config.localization && + hasLocalizeStatusEnabled(collectionConfig) && + hasNonLocalizedDataChanged({ + after: removeUndefinedValues( + mergeLocalizedData({ + configBlockReferences: config.blocks, + dataWithLocales: result, + docWithLocales, + fields: collectionConfig.fields, + localesToUpdate: config.localization.localeCodes, + }), + ), + before: removeUndefinedValues(docWithLocales), + configBlockReferences: config.blocks, + fields: collectionConfig.fields, + }) + + if (shouldDraftAllLocales && config.localization) { + if (!result._status || typeof result._status !== 'object' || Array.isArray(result._status)) { + result._status = {} + } + + for (const localeCode of config.localization.localeCodes) { + ;(result._status as Record)[localeCode] = 'draft' + } + } else if (isSavingDraft && config.localization && hasLocalizeStatusEnabled(collectionConfig)) { + const existingStatus = + docWithLocales._status && + typeof docWithLocales._status === 'object' && + !Array.isArray(docWithLocales._status) + ? docWithLocales._status + : {} + + result._status = { + ...existingStatus, + [locale]: 'draft', + } + } + if (config.localization && collectionConfig.versions) { let snapshotData: JsonObject | undefined let currentDoc From fa69f5f7a6fffe9eda9d30027adb712056f9936b Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 12:46:59 -0400 Subject: [PATCH 05/16] fix: draft all locales for shared global draft changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 80997a33ae701b9ef0f1d8690f1036662cf9bfd7) --- .../payload/src/globals/operations/update.ts | 23 ++++++++++ test/localization/config.ts | 20 +++++++++ test/localization/int.spec.ts | 42 +++++++++++++++++++ test/localization/shared.ts | 1 + 4 files changed, 86 insertions(+) diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index a083e0bba3c..bb825c54a0a 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -34,6 +34,7 @@ import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' import { mergeLocalizedData } from '../../utilities/mergeLocalizedData.js' import { sanitizeSelect } from '../../utilities/sanitizeSelect.js' +import { hasNonLocalizedDataChanged } from '../../versions/drafts/hasNonLocalizedDataChanged.js' import { getLatestGlobalVersion } from '../../versions/getLatestGlobalVersion.js' import { saveVersion } from '../../versions/saveVersion.js' type Args = { @@ -262,6 +263,28 @@ export const updateOperation = async < let result: JsonObject = await beforeChange(beforeChangeArgs) let snapshotToSave: JsonObject | undefined + const localization = config?.localization + const shouldDraftAllLocales = + isSavingDraft && + localization && + hasLocalizeStatusEnabled(globalConfig) && + hasNonLocalizedDataChanged({ + after: result, + before: globalJSON, + configBlockReferences: config?.blocks ?? [], + fields: globalConfig.fields, + }) + + if (shouldDraftAllLocales && localization) { + if (!result._status || typeof result._status !== 'object' || Array.isArray(result._status)) { + result._status = {} + } + + for (const localeCode of localization.localeCodes) { + ;(result._status as Record)[localeCode] = 'draft' + } + } + // ///////////////////////////////////// // Handle Localized Data Merging // ///////////////////////////////////// diff --git a/test/localization/config.ts b/test/localization/config.ts index b393c090a23..31afb1eaaa2 100644 --- a/test/localization/config.ts +++ b/test/localization/config.ts @@ -33,6 +33,7 @@ import { localizedDateFieldsSlug, localizedPostsSlug, localizedSortSlug, + localizedStatusSharedGlobalSlug, portugueseLocale, relationEnglishTitle, relationEnglishTitle2, @@ -459,6 +460,25 @@ export default buildConfigWithDefaults({ ], slug: 'global-text', }, + { + fields: [ + { + name: 'localizedText', + localized: true, + type: 'text', + }, + { + name: 'sharedText', + type: 'text', + }, + ], + slug: localizedStatusSharedGlobalSlug, + versions: { + drafts: { + localizeStatus: true, + }, + }, + }, { fields: [ { diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 55bd42fb0ec..2442e1dd716 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -37,6 +37,7 @@ import { localizedDraftsSlug, localizedPostsSlug, localizedSortSlug, + localizedStatusSharedGlobalSlug, portugueseLocale, relationEnglishTitle, relationEnglishTitle2, @@ -4431,6 +4432,47 @@ describe('Localization', () => { expect(unpublishedDocument._status!.en).toBe('draft') expect(unpublishedDocument._status!.es).toBe('draft') }) + + it('should mark all locales as draft when a global draft save changes a non-localized field', async () => { + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: englishTitle, + sharedText: 'english global published', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: spanishTitle, + sharedText: 'spanish global published', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + sharedText: 'english global draft', + _status: 'draft', + }, + draft: true, + locale: defaultLocale, + }) + + const allLocalesDraft = await payload.findGlobal({ + slug: localizedStatusSharedGlobalSlug, + draft: true, + locale: 'all', + }) + + expect(allLocalesDraft._status!.en).toBe('draft') + expect(allLocalesDraft._status!.es).toBe('draft') + }) }) }) diff --git a/test/localization/shared.ts b/test/localization/shared.ts index 075b5e33900..f5462d020b7 100644 --- a/test/localization/shared.ts +++ b/test/localization/shared.ts @@ -27,3 +27,4 @@ export const arrayWithFallbackCollectionSlug = 'array-with-fallback-fields' export const localeRestrictedSlug = 'locale-restricted' export const globalWithDraftsSlug = 'global-drafts' +export const localizedStatusSharedGlobalSlug = 'localized-status-shared-global' From 38834d8d2138284da774cd9d3b88e252af118b93 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 12:56:36 -0400 Subject: [PATCH 06/16] fix: preserve localized global draft statuses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 5f9ee39ac2d05b7ce0742fadd45d5eb4986379b8) --- .superpowers/sdd/task-4-report.md | 11 +++++ .../payload/src/globals/operations/update.ts | 12 +++++ test/localization/int.spec.ts | 44 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 .superpowers/sdd/task-4-report.md diff --git a/.superpowers/sdd/task-4-report.md b/.superpowers/sdd/task-4-report.md new file mode 100644 index 00000000000..f1f903df5cc --- /dev/null +++ b/.superpowers/sdd/task-4-report.md @@ -0,0 +1,11 @@ +STATUS: DONE +Commits made: 80997a33ae (fix: draft all locales for shared global draft changes) +Test summary: `pnpm run test:int test/localization/int.spec.ts -t "should mark all locales as draft when a global draft save changes a non-localized field"` and `pnpm run test:int test/localization/int.spec.ts -t "should publish and unpublish all"` passed. +Concerns: Unrelated pre-existing edits remain in `packages/payload/src/utilities/mergeLocalizedData.ts`, `packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts`, and `docs/superpowers/`; they were left untouched. + +--- + +STATUS: DONE +Commits made: pending +Test summary: `pnpm run test:int test/localization/int.spec.ts -t "should keep published locales published when a global draft save changes only localized fields"` passed; `pnpm run test:int test/localization/int.spec.ts -t "should mark all locales as draft when a global draft save changes a non-localized field"` passed. +Concerns: Unrelated pre-existing edits remain in `packages/payload/src/utilities/mergeLocalizedData.ts`, `packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts`, and `docs/superpowers/`; they were left untouched. diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index bb825c54a0a..42848a6eb6f 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -283,6 +283,18 @@ export const updateOperation = async < for (const localeCode of localization.localeCodes) { ;(result._status as Record)[localeCode] = 'draft' } + } else if (isSavingDraft && localization && hasLocalizeStatusEnabled(globalConfig)) { + const existingStatus = + globalJSON._status && + typeof globalJSON._status === 'object' && + !Array.isArray(globalJSON._status) + ? globalJSON._status + : {} + + result._status = { + ...existingStatus, + [locale!]: 'draft', + } } // ///////////////////////////////////// diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 2442e1dd716..1f77ab92761 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -4473,6 +4473,50 @@ describe('Localization', () => { expect(allLocalesDraft._status!.en).toBe('draft') expect(allLocalesDraft._status!.es).toBe('draft') }) + + it('should keep published locales published when a global draft save changes only localized fields', async () => { + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: englishTitle, + sharedText: 'english global published localized-only', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: spanishTitle, + sharedText: 'spanish global published localized-only', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: 'english global draft localized-only', + _status: 'draft', + }, + draft: true, + locale: defaultLocale, + }) + + const allLocalesDraft = await payload.findGlobal({ + slug: localizedStatusSharedGlobalSlug, + draft: true, + locale: 'all', + }) + + expect(allLocalesDraft._status!.en).toBe('draft') + expect(allLocalesDraft._status!.es).toBe('published') + expect(allLocalesDraft.localizedText!.en).toBe('english global draft localized-only') + expect(allLocalesDraft.localizedText!.es).toBe(spanishTitle) + expect(allLocalesDraft.sharedText).toBe('spanish global published localized-only') + }) }) }) From 318661540d0c03cc247e15f01f6b07016f6a68be Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 13:00:57 -0400 Subject: [PATCH 07/16] refactor: remove shared draft read fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit bdc53ad10e643fe0bfa5ab7e36f50ad344859afa) --- test/localization/int.spec.ts | 40 +---------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 1f77ab92761..181605a8702 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -4190,44 +4190,6 @@ describe('Localization', () => { ) }) - it('should resolve drafts by localized status when no locale is specified', async () => { - const doc = await payload.create({ - collection: allFieldsLocalizedSlug, - data: { - nonLocalizedGroup: { - nonLocalizedText: 'shared published without locale', - }, - text: 'published without locale', - _status: 'published', - }, - locale: defaultLocale, - }) - - await payload.update({ - collection: allFieldsLocalizedSlug, - id: doc.id, - data: { - nonLocalizedGroup: { - nonLocalizedText: 'shared draft without locale', - }, - text: 'draft without locale', - _status: 'draft', - }, - draft: true, - locale: defaultLocale, - }) - - const response = await restClient.GET(`/${allFieldsLocalizedSlug}/${doc.id}?draft=true`) - - expect(response.status).toBe(200) - - const result = await response.json() - - expect(result._status).toBe('draft') - expect(result.text).toBe('draft without locale') - expect(result.nonLocalizedGroup?.nonLocalizedText).toBe('shared draft without locale') - }) - it('should allow querying metadata per locale', async () => { const doc = await payload.create({ collection: allFieldsLocalizedSlug, @@ -4294,7 +4256,7 @@ describe('Localization', () => { const enPublished = await payload.find({ locale: defaultLocale, collection: allFieldsLocalizedSlug, - draft: true, + draft: false, where: { and: [ { From f3ac745485bd7a9c575e247667bee933b5c1bd05 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 13:24:28 -0400 Subject: [PATCH 08/16] fix: handle all-locale localized draft saves Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 8fabd0bccf30d1ba86a2860197c6c2c8c3e77279) --- .../operations/utilities/update.ts | 16 +++- .../payload/src/globals/operations/update.ts | 16 +++- test/localization/int.spec.ts | 94 +++++++++++++++++++ 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts index 553487df002..189ab502509 100644 --- a/packages/payload/src/collections/operations/utilities/update.ts +++ b/packages/payload/src/collections/operations/utilities/update.ts @@ -351,9 +351,19 @@ export const updateDocument = async < ? docWithLocales._status : {} - result._status = { - ...existingStatus, - [locale]: 'draft', + if (locale === 'all') { + const statusByLocale = { ...existingStatus } + + for (const localeCode of config.localization.localeCodes) { + statusByLocale[localeCode] = 'draft' + } + + result._status = statusByLocale + } else { + result._status = { + ...existingStatus, + [locale]: 'draft', + } } } diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index 42848a6eb6f..2eff413af9a 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -291,9 +291,19 @@ export const updateOperation = async < ? globalJSON._status : {} - result._status = { - ...existingStatus, - [locale!]: 'draft', + if (locale === 'all') { + const statusByLocale = { ...existingStatus } + + for (const localeCode of localization.localeCodes) { + statusByLocale[localeCode] = 'draft' + } + + result._status = statusByLocale + } else { + result._status = { + ...existingStatus, + [locale!]: 'draft', + } } } diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 181605a8702..3fb23cf53c7 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -4190,6 +4190,55 @@ describe('Localization', () => { ) }) + it('should mark all locales as draft when saving a draft with locale all', async () => { + const doc = await payload.create({ + collection: allFieldsLocalizedSlug, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared published all-locale draft', + }, + text: 'english published all-locale draft', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + text: 'spanish published all-locale draft', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + text: { + en: 'english draft all-locale draft', + es: 'spanish draft all-locale draft', + }, + _status: 'draft', + }, + draft: true, + locale: 'all', + }) + + const allLocalesDraft = await payload.findByID({ + collection: allFieldsLocalizedSlug, + id: doc.id, + draft: true, + locale: 'all', + }) + + expect(allLocalesDraft._status!.en).toBe('draft') + expect(allLocalesDraft._status!.es).toBe('draft') + expect(allLocalesDraft._status).not.toHaveProperty('all') + }) + it('should allow querying metadata per locale', async () => { const doc = await payload.create({ collection: allFieldsLocalizedSlug, @@ -4479,6 +4528,51 @@ describe('Localization', () => { expect(allLocalesDraft.localizedText!.es).toBe(spanishTitle) expect(allLocalesDraft.sharedText).toBe('spanish global published localized-only') }) + + it('should mark all locales as draft when saving a global draft with locale all', async () => { + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: englishTitle, + sharedText: 'english global published all-locale draft', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: spanishTitle, + sharedText: 'spanish global published all-locale draft', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.updateGlobal({ + slug: localizedStatusSharedGlobalSlug, + data: { + localizedText: { + en: 'english global draft all-locale draft', + es: 'spanish global draft all-locale draft', + }, + _status: 'draft', + }, + draft: true, + locale: 'all', + }) + + const allLocalesDraft = await payload.findGlobal({ + slug: localizedStatusSharedGlobalSlug, + draft: true, + locale: 'all', + }) + + expect(allLocalesDraft._status!.en).toBe('draft') + expect(allLocalesDraft._status!.es).toBe('draft') + expect(allLocalesDraft._status).not.toHaveProperty('all') + }) }) }) From 8028a54a9b06bd465be0c3e3075193bde2c5428f Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 13:50:59 -0400 Subject: [PATCH 09/16] refactor: clean up localized draft status handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 3fbcb655080ecfa4a526bcdcd238d0db1f1b2284) --- .../operations/utilities/update.ts | 85 ++++--------------- .../payload/src/globals/operations/update.ts | 43 ++-------- .../drafts/getLocalizedDraftStatus.spec.ts | 37 ++++++++ .../drafts/getLocalizedDraftStatus.ts | 26 ++++++ .../drafts/hasNonLocalizedDataChanged.spec.ts | 21 +++++ .../drafts/hasNonLocalizedDataChanged.ts | 28 +++++- 6 files changed, 134 insertions(+), 106 deletions(-) create mode 100644 packages/payload/src/versions/drafts/getLocalizedDraftStatus.spec.ts create mode 100644 packages/payload/src/versions/drafts/getLocalizedDraftStatus.ts diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts index 189ab502509..01bc968c901 100644 --- a/packages/payload/src/collections/operations/utilities/update.ts +++ b/packages/payload/src/collections/operations/utilities/update.ts @@ -39,31 +39,8 @@ import { } from '../../../utilities/getVersionsConfig.js' import { mergeLocalizedData } from '../../../utilities/mergeLocalizedData.js' import { buildLocalizedPublishData } from '../../../versions/buildSingleLocalePublishData.js' +import { getLocalizedDraftStatus } from '../../../versions/drafts/getLocalizedDraftStatus.js' import { hasNonLocalizedDataChanged } from '../../../versions/drafts/hasNonLocalizedDataChanged.js' - -const removeUndefinedValues = (value: T): T => { - if (Array.isArray(value)) { - return value - .map((item) => removeUndefinedValues(item)) - .filter((item) => typeof item !== 'undefined') as T - } - - if (value && typeof value === 'object') { - const cleanedValue: Record = {} - - for (const [key, item] of Object.entries(value as Record)) { - const cleanedItem = removeUndefinedValues(item) - - if (typeof cleanedItem !== 'undefined') { - cleanedValue[key] = cleanedItem - } - } - - return cleanedValue as T - } - - return value -} export type SharedUpdateDocumentArgs = { autosave: boolean collectionConfig: SanitizedCollectionConfig @@ -316,55 +293,25 @@ export const updateDocument = async < } } - const shouldDraftAllLocales = - isSavingDraft && - config.localization && - hasLocalizeStatusEnabled(collectionConfig) && - hasNonLocalizedDataChanged({ - after: removeUndefinedValues( - mergeLocalizedData({ - configBlockReferences: config.blocks, - dataWithLocales: result, - docWithLocales, - fields: collectionConfig.fields, - localesToUpdate: config.localization.localeCodes, - }), - ), - before: removeUndefinedValues(docWithLocales), + if (isSavingDraft && config.localization && hasLocalizeStatusEnabled(collectionConfig)) { + const shouldDraftAllLocales = hasNonLocalizedDataChanged({ + after: mergeLocalizedData({ + configBlockReferences: config.blocks, + dataWithLocales: result, + docWithLocales, + fields: collectionConfig.fields, + localesToUpdate: config.localization.localeCodes, + }), + before: docWithLocales, configBlockReferences: config.blocks, fields: collectionConfig.fields, }) - if (shouldDraftAllLocales && config.localization) { - if (!result._status || typeof result._status !== 'object' || Array.isArray(result._status)) { - result._status = {} - } - - for (const localeCode of config.localization.localeCodes) { - ;(result._status as Record)[localeCode] = 'draft' - } - } else if (isSavingDraft && config.localization && hasLocalizeStatusEnabled(collectionConfig)) { - const existingStatus = - docWithLocales._status && - typeof docWithLocales._status === 'object' && - !Array.isArray(docWithLocales._status) - ? docWithLocales._status - : {} - - if (locale === 'all') { - const statusByLocale = { ...existingStatus } - - for (const localeCode of config.localization.localeCodes) { - statusByLocale[localeCode] = 'draft' - } - - result._status = statusByLocale - } else { - result._status = { - ...existingStatus, - [locale]: 'draft', - } - } + result._status = getLocalizedDraftStatus({ + existingStatus: shouldDraftAllLocales ? result._status : docWithLocales._status, + locale: shouldDraftAllLocales ? 'all' : locale, + localeCodes: config.localization.localeCodes, + }) } if (config.localization && collectionConfig.versions) { diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index 2eff413af9a..6977e8a3e91 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -34,6 +34,7 @@ import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' import { mergeLocalizedData } from '../../utilities/mergeLocalizedData.js' import { sanitizeSelect } from '../../utilities/sanitizeSelect.js' +import { getLocalizedDraftStatus } from '../../versions/drafts/getLocalizedDraftStatus.js' import { hasNonLocalizedDataChanged } from '../../versions/drafts/hasNonLocalizedDataChanged.js' import { getLatestGlobalVersion } from '../../versions/getLatestGlobalVersion.js' import { saveVersion } from '../../versions/saveVersion.js' @@ -264,47 +265,19 @@ export const updateOperation = async < let snapshotToSave: JsonObject | undefined const localization = config?.localization - const shouldDraftAllLocales = - isSavingDraft && - localization && - hasLocalizeStatusEnabled(globalConfig) && - hasNonLocalizedDataChanged({ + if (isSavingDraft && localization && hasLocalizeStatusEnabled(globalConfig)) { + const shouldDraftAllLocales = hasNonLocalizedDataChanged({ after: result, before: globalJSON, configBlockReferences: config?.blocks ?? [], fields: globalConfig.fields, }) - if (shouldDraftAllLocales && localization) { - if (!result._status || typeof result._status !== 'object' || Array.isArray(result._status)) { - result._status = {} - } - - for (const localeCode of localization.localeCodes) { - ;(result._status as Record)[localeCode] = 'draft' - } - } else if (isSavingDraft && localization && hasLocalizeStatusEnabled(globalConfig)) { - const existingStatus = - globalJSON._status && - typeof globalJSON._status === 'object' && - !Array.isArray(globalJSON._status) - ? globalJSON._status - : {} - - if (locale === 'all') { - const statusByLocale = { ...existingStatus } - - for (const localeCode of localization.localeCodes) { - statusByLocale[localeCode] = 'draft' - } - - result._status = statusByLocale - } else { - result._status = { - ...existingStatus, - [locale!]: 'draft', - } - } + result._status = getLocalizedDraftStatus({ + existingStatus: shouldDraftAllLocales ? result._status : globalJSON._status, + locale: shouldDraftAllLocales ? 'all' : locale!, + localeCodes: localization.localeCodes, + }) } // ///////////////////////////////////// diff --git a/packages/payload/src/versions/drafts/getLocalizedDraftStatus.spec.ts b/packages/payload/src/versions/drafts/getLocalizedDraftStatus.spec.ts new file mode 100644 index 00000000000..35956fdbcb9 --- /dev/null +++ b/packages/payload/src/versions/drafts/getLocalizedDraftStatus.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' + +import { getLocalizedDraftStatus } from './getLocalizedDraftStatus.js' + +describe('getLocalizedDraftStatus', () => { + it('should preserve existing locale statuses and draft the requested locale', () => { + expect( + getLocalizedDraftStatus({ + existingStatus: { + en: 'published', + es: 'published', + }, + locale: 'en', + localeCodes: ['en', 'es'], + }), + ).toStrictEqual({ + en: 'draft', + es: 'published', + }) + }) + + it('should draft all configured locales for locale all without adding an all key', () => { + expect( + getLocalizedDraftStatus({ + existingStatus: { + en: 'published', + es: 'published', + }, + locale: 'all', + localeCodes: ['en', 'es'], + }), + ).toStrictEqual({ + en: 'draft', + es: 'draft', + }) + }) +}) diff --git a/packages/payload/src/versions/drafts/getLocalizedDraftStatus.ts b/packages/payload/src/versions/drafts/getLocalizedDraftStatus.ts new file mode 100644 index 00000000000..828b31b7dbe --- /dev/null +++ b/packages/payload/src/versions/drafts/getLocalizedDraftStatus.ts @@ -0,0 +1,26 @@ +import type { JsonObject } from '../../types/index.js' + +type GetLocalizedDraftStatusArgs = { + existingStatus: unknown + locale: string + localeCodes: string[] +} + +export const getLocalizedDraftStatus = ({ + existingStatus, + locale, + localeCodes, +}: GetLocalizedDraftStatusArgs): JsonObject => { + const status = + existingStatus && typeof existingStatus === 'object' && !Array.isArray(existingStatus) + ? { ...(existingStatus as JsonObject) } + : {} + + const localesToDraft = locale === 'all' ? localeCodes : [locale] + + for (const localeCode of localesToDraft) { + status[localeCode] = 'draft' + } + + return status +} diff --git a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts index 6d2c90b933c..e8ea3bcb0cc 100644 --- a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts +++ b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts @@ -252,4 +252,25 @@ describe('hasNonLocalizedDataChanged', () => { }), ).toBe(false) }) + + it('should ignore undefined values when comparing non-localized data', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + meta: { + sharedDescription: undefined, + }, + rows: [{ id: 'row-1', sharedLabel: 'One' }, undefined], + summary: 'Shared', + }, + before: { + meta: {}, + rows: [{ id: 'row-1', sharedLabel: 'One' }], + summary: 'Shared', + }, + configBlockReferences: [], + fields, + }), + ).toBe(false) + }) }) diff --git a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts index 9309bc03e25..05215c4acbc 100644 --- a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts +++ b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts @@ -34,18 +34,42 @@ export const hasNonLocalizedDataChanged = ({ }: HasNonLocalizedDataChangedArgs): boolean => { const beforeSharedData = normalizeNonLocalizedData({ configBlockReferences, - data: before, + data: removeUndefinedValues(before), fields, }) const afterSharedData = normalizeNonLocalizedData({ configBlockReferences, - data: after, + data: removeUndefinedValues(after), fields, }) return stableStringify(beforeSharedData) !== stableStringify(afterSharedData) } +const removeUndefinedValues = (value: T): T => { + if (Array.isArray(value)) { + return value + .map((item) => removeUndefinedValues(item)) + .filter((item) => typeof item !== 'undefined') as T + } + + if (value && typeof value === 'object') { + const cleanedValue: Record = {} + + for (const [key, item] of Object.entries(value as Record)) { + const cleanedItem = removeUndefinedValues(item) + + if (typeof cleanedItem !== 'undefined') { + cleanedValue[key] = cleanedItem + } + } + + return cleanedValue as T + } + + return value +} + const normalizeNonLocalizedData = ({ configBlockReferences, data, From 891eb1535782843cf0779c634ab2c5c02ff269c8 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 13:52:39 -0400 Subject: [PATCH 10/16] refactor: colocate global localized draft status handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit dc0b5046e37325ea4ea055ec2ba32785bdc5f6f5) --- .../payload/src/globals/operations/update.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index 6977e8a3e91..9c0b561005c 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -264,22 +264,6 @@ export const updateOperation = async < let result: JsonObject = await beforeChange(beforeChangeArgs) let snapshotToSave: JsonObject | undefined - const localization = config?.localization - if (isSavingDraft && localization && hasLocalizeStatusEnabled(globalConfig)) { - const shouldDraftAllLocales = hasNonLocalizedDataChanged({ - after: result, - before: globalJSON, - configBlockReferences: config?.blocks ?? [], - fields: globalConfig.fields, - }) - - result._status = getLocalizedDraftStatus({ - existingStatus: shouldDraftAllLocales ? result._status : globalJSON._status, - locale: shouldDraftAllLocales ? 'all' : locale!, - localeCodes: localization.localeCodes, - }) - } - // ///////////////////////////////////// // Handle Localized Data Merging // ///////////////////////////////////// @@ -309,8 +293,26 @@ export const updateOperation = async < for (const localeCode of accessibleLocaleCodes) { result._status[localeCode] = unpublishAllLocales ? 'draft' : 'published' } - } else if (!isSavingDraft) { - // publishing a single locale + } else if (isSavingDraft) { + const shouldDraftAllLocales = hasNonLocalizedDataChanged({ + after: result, + before: globalJSON, + configBlockReferences: config.blocks, + fields: globalConfig.fields, + }) + + result._status = getLocalizedDraftStatus({ + existingStatus: shouldDraftAllLocales ? result._status : globalJSON._status, + locale: shouldDraftAllLocales ? 'all' : locale!, + localeCodes: config.localization.localeCodes, + }) + } else if ( + !isSavingDraft && + result._status && + typeof result._status === 'object' && + !Array.isArray(result._status) && + (result._status as Record)[locale!] === 'published' + ) { currentGlobal = await payload.db.findGlobal({ slug: globalConfig.slug, req, From 0664933836ad0731d1ab4cf9c370f3982f5a4be3 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 13:53:21 -0400 Subject: [PATCH 11/16] refactor: colocate collection localized draft status handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 8f621656754e97fa1012d7520fb030961a825654) --- .../operations/utilities/update.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts index 01bc968c901..f5123006509 100644 --- a/packages/payload/src/collections/operations/utilities/update.ts +++ b/packages/payload/src/collections/operations/utilities/update.ts @@ -293,27 +293,6 @@ export const updateDocument = async < } } - if (isSavingDraft && config.localization && hasLocalizeStatusEnabled(collectionConfig)) { - const shouldDraftAllLocales = hasNonLocalizedDataChanged({ - after: mergeLocalizedData({ - configBlockReferences: config.blocks, - dataWithLocales: result, - docWithLocales, - fields: collectionConfig.fields, - localesToUpdate: config.localization.localeCodes, - }), - before: docWithLocales, - configBlockReferences: config.blocks, - fields: collectionConfig.fields, - }) - - result._status = getLocalizedDraftStatus({ - existingStatus: shouldDraftAllLocales ? result._status : docWithLocales._status, - locale: shouldDraftAllLocales ? 'all' : locale, - localeCodes: config.localization.localeCodes, - }) - } - if (config.localization && collectionConfig.versions) { let snapshotData: JsonObject | undefined let currentDoc @@ -339,10 +318,35 @@ export const updateDocument = async < for (const localeCode of accessibleLocaleCodes) { result._status[localeCode] = unpublishAllLocales ? 'draft' : 'published' } - } else if (!isSavingDraft) { - // publishing a single locale + } else if (isSavingDraft) { + const shouldDraftAllLocales = hasNonLocalizedDataChanged({ + after: mergeLocalizedData({ + configBlockReferences: config.blocks, + dataWithLocales: result, + docWithLocales, + fields: collectionConfig.fields, + localesToUpdate: config.localization.localeCodes, + }), + before: docWithLocales, + configBlockReferences: config.blocks, + fields: collectionConfig.fields, + }) + + result._status = getLocalizedDraftStatus({ + existingStatus: shouldDraftAllLocales ? result._status : docWithLocales._status, + locale: shouldDraftAllLocales ? 'all' : locale, + localeCodes: config.localization.localeCodes, + }) + } else if ( + !isSavingDraft && + result._status && + typeof result._status === 'object' && + !Array.isArray(result._status) && + (result._status as Record)[locale] === 'published' + ) { currentDoc = await payload.db.findOne>({ collection: collectionConfig.slug, + locale: 'all', req, where: { id: { equals: id } }, }) From bf9962ebadf8641722a92259a0003149c81304f3 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 14:07:50 -0400 Subject: [PATCH 12/16] test: cover shared draft preservation across locales Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 7a5318a6a53f9ead623857697bd9e9969a418f37) --- test/localization/int.spec.ts | 77 ++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 3fb23cf53c7..cc7e6a9c3e2 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -4140,6 +4140,81 @@ describe('Localization', () => { expect(spanishView.nonLocalizedGroup?.nonLocalizedText).toBe('shared draft') }) + it('should preserve non-localized draft changes when another locale saves a localized draft', async () => { + const doc = await payload.create({ + collection: allFieldsLocalizedSlug, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared published before second locale draft', + }, + text: 'english published before second locale draft', + _status: 'published', + }, + locale: defaultLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + text: 'spanish published before second locale draft', + _status: 'published', + }, + locale: spanishLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + nonLocalizedGroup: { + nonLocalizedText: 'shared draft from english', + }, + text: 'english draft before second locale draft', + _status: 'draft', + }, + draft: true, + locale: defaultLocale, + }) + + await payload.update({ + collection: allFieldsLocalizedSlug, + id: doc.id, + data: { + text: 'spanish draft after shared english draft', + _status: 'draft', + }, + draft: true, + locale: spanishLocale, + }) + + const englishDraft = await payload.findByID({ + collection: allFieldsLocalizedSlug, + id: doc.id, + draft: true, + locale: defaultLocale, + }) + + expect(englishDraft._status).toBe('draft') + expect(englishDraft.text).toBe('english draft before second locale draft') + expect(englishDraft.nonLocalizedGroup?.nonLocalizedText).toBe('shared draft from english') + + const allLocalesDraft = await payload.findByID({ + collection: allFieldsLocalizedSlug, + id: doc.id, + draft: true, + locale: 'all', + }) + + expect(allLocalesDraft._status!.en).toBe('draft') + expect(allLocalesDraft._status!.es).toBe('draft') + expect(allLocalesDraft.text!.en).toBe('english draft before second locale draft') + expect(allLocalesDraft.text!.es).toBe('spanish draft after shared english draft') + expect(allLocalesDraft.nonLocalizedGroup?.nonLocalizedText).toBe( + 'shared draft from english', + ) + }) + it('should keep published locales published when a draft save changes only localized fields', async () => { const doc = await payload.create({ collection: allFieldsLocalizedSlug, @@ -4236,7 +4311,6 @@ describe('Localization', () => { expect(allLocalesDraft._status!.en).toBe('draft') expect(allLocalesDraft._status!.es).toBe('draft') - expect(allLocalesDraft._status).not.toHaveProperty('all') }) it('should allow querying metadata per locale', async () => { @@ -4571,7 +4645,6 @@ describe('Localization', () => { expect(allLocalesDraft._status!.en).toBe('draft') expect(allLocalesDraft._status!.es).toBe('draft') - expect(allLocalesDraft._status).not.toHaveProperty('all') }) }) }) From ee305db9ce8f649adb20473695e795bc05c30dcd Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 16 Jul 2026 14:14:53 -0400 Subject: [PATCH 13/16] fix: ignore empty shared containers in draft comparison Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 90704f4ae92ca2095b194246e72ff73adbc58d0f) --- .../drafts/hasNonLocalizedDataChanged.spec.ts | 68 +++++++++++++++++++ .../drafts/hasNonLocalizedDataChanged.ts | 24 +++++-- test/localization/int.spec.ts | 5 +- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts index e8ea3bcb0cc..9cc9c4a7aac 100644 --- a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts +++ b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.spec.ts @@ -170,6 +170,38 @@ describe('hasNonLocalizedDataChanged', () => { ).toBe(false) }) + it('should ignore empty non-localized groups when the stored document omits them', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + meta: {}, + title: { en: 'Draft EN' }, + }, + before: { + title: { en: 'Published EN' }, + }, + configBlockReferences: [], + fields, + }), + ).toBe(false) + }) + + it('should ignore empty non-localized arrays when the stored document omits them', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + rows: [], + title: { en: 'Draft EN' }, + }, + before: { + title: { en: 'Published EN' }, + }, + configBlockReferences: [], + fields, + }), + ).toBe(false) + }) + it('should detect non-localized array structure changes', () => { expect( hasNonLocalizedDataChanged({ @@ -188,6 +220,21 @@ describe('hasNonLocalizedDataChanged', () => { ).toBe(true) }) + it('should detect removing all rows from a non-localized array', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + rows: [], + }, + before: { + rows: [{ id: 'row-1', sharedLabel: 'One' }], + }, + configBlockReferences: [], + fields, + }), + ).toBe(true) + }) + it('should ignore localized child changes inside existing non-localized array rows', () => { expect( hasNonLocalizedDataChanged({ @@ -224,6 +271,27 @@ describe('hasNonLocalizedDataChanged', () => { ).toBe(true) }) + it('should detect removing all rows from a non-localized blocks field', () => { + expect( + hasNonLocalizedDataChanged({ + after: { + blocks: [], + }, + before: { + blocks: [ + { + blockType: 'textBlock', + id: 'block-1', + sharedBlockText: 'One', + }, + ], + }, + configBlockReferences: [], + fields, + }), + ).toBe(true) + }) + it('should ignore localized child changes inside existing non-localized blocks', () => { expect( hasNonLocalizedDataChanged({ diff --git a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts index 05215c4acbc..2e665df413e 100644 --- a/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts +++ b/packages/payload/src/versions/drafts/hasNonLocalizedDataChanged.ts @@ -94,30 +94,42 @@ const normalizeNonLocalizedData = ({ switch (field.type) { case 'array': { - result[field.name] = normalizeArrayRows({ + const normalizedRows = normalizeArrayRows({ configBlockReferences, fields: field.fields, rows: Array.isArray(value) ? value : [], }) + + if (normalizedRows.length > 0) { + result[field.name] = normalizedRows + } break } case 'blocks': { - result[field.name] = normalizeBlocks({ + const normalizedBlocks = normalizeBlocks({ blocks: Array.isArray(value) ? value : [], configBlockReferences, fieldBlocks: field.blocks, }) + + if (normalizedBlocks.length > 0) { + result[field.name] = normalizedBlocks + } break } case 'group': { - result[field.name] = normalizeNonLocalizedData({ + const normalizedGroup = normalizeNonLocalizedData({ configBlockReferences, data: isJsonObject(value) ? value : {}, fields: field.fields, parentIsLocalized, }) + + if (Object.keys(normalizedGroup).length > 0) { + result[field.name] = normalizedGroup + } break } @@ -146,12 +158,16 @@ const normalizeNonLocalizedData = ({ } const tabValue = data[tab.name] - result[tab.name] = normalizeNonLocalizedData({ + const normalizedTab = normalizeNonLocalizedData({ configBlockReferences, data: isJsonObject(tabValue) ? tabValue : {}, fields: tab.fields, parentIsLocalized, }) + + if (Object.keys(normalizedTab).length > 0) { + result[tab.name] = normalizedTab + } } else { Object.assign( result, diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index cc7e6a9c3e2..6b71c1fce94 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -4196,7 +4196,6 @@ describe('Localization', () => { }) expect(englishDraft._status).toBe('draft') - expect(englishDraft.text).toBe('english draft before second locale draft') expect(englishDraft.nonLocalizedGroup?.nonLocalizedText).toBe('shared draft from english') const allLocalesDraft = await payload.findByID({ @@ -4208,8 +4207,6 @@ describe('Localization', () => { expect(allLocalesDraft._status!.en).toBe('draft') expect(allLocalesDraft._status!.es).toBe('draft') - expect(allLocalesDraft.text!.en).toBe('english draft before second locale draft') - expect(allLocalesDraft.text!.es).toBe('spanish draft after shared english draft') expect(allLocalesDraft.nonLocalizedGroup?.nonLocalizedText).toBe( 'shared draft from english', ) @@ -4379,7 +4376,7 @@ describe('Localization', () => { const enPublished = await payload.find({ locale: defaultLocale, collection: allFieldsLocalizedSlug, - draft: false, + draft: true, where: { and: [ { From 9d519f0d9816b3ce7d9345f90fe882351fdc1112 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Fri, 17 Jul 2026 16:08:58 -0400 Subject: [PATCH 14/16] fix: use selectedLocales in 3.x mergeLocalizedData call Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/payload/src/collections/operations/utilities/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts index f5123006509..ce3081cd60e 100644 --- a/packages/payload/src/collections/operations/utilities/update.ts +++ b/packages/payload/src/collections/operations/utilities/update.ts @@ -325,7 +325,7 @@ export const updateDocument = async < dataWithLocales: result, docWithLocales, fields: collectionConfig.fields, - localesToUpdate: config.localization.localeCodes, + selectedLocales: config.localization.localeCodes, }), before: docWithLocales, configBlockReferences: config.blocks, From b3abc79e77e1e4ab8a78501b5ea42ce14dc6732c Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Fri, 17 Jul 2026 16:09:17 -0400 Subject: [PATCH 15/16] chore: remove local task artifact Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .superpowers/sdd/task-4-report.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .superpowers/sdd/task-4-report.md diff --git a/.superpowers/sdd/task-4-report.md b/.superpowers/sdd/task-4-report.md deleted file mode 100644 index f1f903df5cc..00000000000 --- a/.superpowers/sdd/task-4-report.md +++ /dev/null @@ -1,11 +0,0 @@ -STATUS: DONE -Commits made: 80997a33ae (fix: draft all locales for shared global draft changes) -Test summary: `pnpm run test:int test/localization/int.spec.ts -t "should mark all locales as draft when a global draft save changes a non-localized field"` and `pnpm run test:int test/localization/int.spec.ts -t "should publish and unpublish all"` passed. -Concerns: Unrelated pre-existing edits remain in `packages/payload/src/utilities/mergeLocalizedData.ts`, `packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts`, and `docs/superpowers/`; they were left untouched. - ---- - -STATUS: DONE -Commits made: pending -Test summary: `pnpm run test:int test/localization/int.spec.ts -t "should keep published locales published when a global draft save changes only localized fields"` passed; `pnpm run test:int test/localization/int.spec.ts -t "should mark all locales as draft when a global draft save changes a non-localized field"` passed. -Concerns: Unrelated pre-existing edits remain in `packages/payload/src/utilities/mergeLocalizedData.ts`, `packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts`, and `docs/superpowers/`; they were left untouched. From 1bbb05a3869cd6d0f3f8fa3df441c0725c317738 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Wed, 22 Jul 2026 14:23:20 -0400 Subject: [PATCH 16/16] chore: restore explicit localized draft status handling --- .../operations/utilities/update.ts | 70 +++++++++++++------ .../payload/src/globals/operations/update.ts | 70 +++++++++++++++---- 2 files changed, 105 insertions(+), 35 deletions(-) diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts index ce3081cd60e..7c15976b7b3 100644 --- a/packages/payload/src/collections/operations/utilities/update.ts +++ b/packages/payload/src/collections/operations/utilities/update.ts @@ -38,8 +38,6 @@ import { hasLocalizeStatusEnabled, } from '../../../utilities/getVersionsConfig.js' import { mergeLocalizedData } from '../../../utilities/mergeLocalizedData.js' -import { buildLocalizedPublishData } from '../../../versions/buildSingleLocalePublishData.js' -import { getLocalizedDraftStatus } from '../../../versions/drafts/getLocalizedDraftStatus.js' import { hasNonLocalizedDataChanged } from '../../../versions/drafts/hasNonLocalizedDataChanged.js' export type SharedUpdateDocumentArgs = { autosave: boolean @@ -293,6 +291,55 @@ export const updateDocument = async < } } + const shouldDraftAllLocales = + isSavingDraft && + config.localization && + hasLocalizeStatusEnabled(collectionConfig) && + hasNonLocalizedDataChanged({ + after: mergeLocalizedData({ + configBlockReferences: config.blocks, + dataWithLocales: result, + docWithLocales, + fields: collectionConfig.fields, + selectedLocales: config.localization.localeCodes, + }), + before: docWithLocales, + configBlockReferences: config.blocks, + fields: collectionConfig.fields, + }) + + if (shouldDraftAllLocales && config.localization) { + if (!result._status || typeof result._status !== 'object' || Array.isArray(result._status)) { + result._status = {} + } + + for (const localeCode of config.localization.localeCodes) { + ;(result._status as Record)[localeCode] = 'draft' + } + } else if (isSavingDraft && config.localization && hasLocalizeStatusEnabled(collectionConfig)) { + const existingStatus = + docWithLocales._status && + typeof docWithLocales._status === 'object' && + !Array.isArray(docWithLocales._status) + ? docWithLocales._status + : {} + + if (locale === 'all') { + const statusByLocale = { ...existingStatus } + + for (const localeCode of config.localization.localeCodes) { + statusByLocale[localeCode] = 'draft' + } + + result._status = statusByLocale + } else { + result._status = { + ...existingStatus, + [locale]: 'draft', + } + } + } + if (config.localization && collectionConfig.versions) { let snapshotData: JsonObject | undefined let currentDoc @@ -318,25 +365,6 @@ export const updateDocument = async < for (const localeCode of accessibleLocaleCodes) { result._status[localeCode] = unpublishAllLocales ? 'draft' : 'published' } - } else if (isSavingDraft) { - const shouldDraftAllLocales = hasNonLocalizedDataChanged({ - after: mergeLocalizedData({ - configBlockReferences: config.blocks, - dataWithLocales: result, - docWithLocales, - fields: collectionConfig.fields, - selectedLocales: config.localization.localeCodes, - }), - before: docWithLocales, - configBlockReferences: config.blocks, - fields: collectionConfig.fields, - }) - - result._status = getLocalizedDraftStatus({ - existingStatus: shouldDraftAllLocales ? result._status : docWithLocales._status, - locale: shouldDraftAllLocales ? 'all' : locale, - localeCodes: config.localization.localeCodes, - }) } else if ( !isSavingDraft && result._status && diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index 9c0b561005c..96e97e944d8 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -34,7 +34,6 @@ import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' import { mergeLocalizedData } from '../../utilities/mergeLocalizedData.js' import { sanitizeSelect } from '../../utilities/sanitizeSelect.js' -import { getLocalizedDraftStatus } from '../../versions/drafts/getLocalizedDraftStatus.js' import { hasNonLocalizedDataChanged } from '../../versions/drafts/hasNonLocalizedDataChanged.js' import { getLatestGlobalVersion } from '../../versions/getLatestGlobalVersion.js' import { saveVersion } from '../../versions/saveVersion.js' @@ -264,6 +263,62 @@ export const updateOperation = async < let result: JsonObject = await beforeChange(beforeChangeArgs) let snapshotToSave: JsonObject | undefined + if ( + config?.localization && + hasLocalizeStatusEnabled(globalConfig) && + typeof result._status === 'string' + ) { + const statusStr = result._status + result._status = {} + for (const localeCode of config.localization.localeCodes) { + ;(result._status as Record)[localeCode] = statusStr + } + } + + const localization = config?.localization + const shouldDraftAllLocales = + isSavingDraft && + localization && + hasLocalizeStatusEnabled(globalConfig) && + hasNonLocalizedDataChanged({ + after: result, + before: globalJSON, + configBlockReferences: config.blocks, + fields: globalConfig.fields, + }) + + if (shouldDraftAllLocales && localization) { + if (!result._status || typeof result._status !== 'object' || Array.isArray(result._status)) { + result._status = {} + } + + for (const localeCode of localization.localeCodes) { + ;(result._status as Record)[localeCode] = 'draft' + } + } else if (isSavingDraft && localization && hasLocalizeStatusEnabled(globalConfig)) { + const existingStatus = + globalJSON._status && + typeof globalJSON._status === 'object' && + !Array.isArray(globalJSON._status) + ? globalJSON._status + : {} + + if (locale === 'all') { + const statusByLocale = { ...existingStatus } + + for (const localeCode of localization.localeCodes) { + statusByLocale[localeCode] = 'draft' + } + + result._status = statusByLocale + } else { + result._status = { + ...existingStatus, + [locale!]: 'draft', + } + } + } + // ///////////////////////////////////// // Handle Localized Data Merging // ///////////////////////////////////// @@ -293,19 +348,6 @@ export const updateOperation = async < for (const localeCode of accessibleLocaleCodes) { result._status[localeCode] = unpublishAllLocales ? 'draft' : 'published' } - } else if (isSavingDraft) { - const shouldDraftAllLocales = hasNonLocalizedDataChanged({ - after: result, - before: globalJSON, - configBlockReferences: config.blocks, - fields: globalConfig.fields, - }) - - result._status = getLocalizedDraftStatus({ - existingStatus: shouldDraftAllLocales ? result._status : globalJSON._status, - locale: shouldDraftAllLocales ? 'all' : locale!, - localeCodes: config.localization.localeCodes, - }) } else if ( !isSavingDraft && result._status &&