Skip to content

Commit 7a3f43f

Browse files
authored
fix: prevent silent data overwrites on concurrent edits (#15749)
### What Adds stale data detection to prevent silent overwrites when multiple users edit the same document. ### Why Currently, when two users edit the same document, the last save wins with no warning. User 1 can make changes, then User 2 saves, overwriting User 1's work silently. This is separate from document locking and addresses a specific scenario: 1. Both users already have the same document open at the same time. 2. User 1 makes changes and saves. 3. User 2, still viewing their original version, then decides to make edits. 4. Without this fix, User 2 would unknowingly overwrite User 1's changes with no warning. ### How Now we track the document's `updatedAt` timestamp when it initially loads. On first edit, check if the database version has a newer timestamp. If stale, show a modal with the option to reload (get latest). The check only happens once per edit session to avoid excessive DB queries. <img width="693" height="360" alt="Screenshot 2026-02-24 at 1 37 01 PM" src="https://github.com/user-attachments/assets/1d63973c-fa40-4213-8718-9ea739f267c9" /> Fixes #15486
1 parent 6557292 commit 7a3f43f

File tree

59 files changed

+1267
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1267
-9
lines changed

packages/payload/src/admin/forms/Form.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ export type FormStateWithoutComponents = {
101101
}
102102

103103
export type BuildFormStateArgs = {
104+
/**
105+
* If true, will check if the document has been modified since it was loaded.
106+
* This helps detect stale data when multiple users are editing the same document.
107+
*/
108+
checkForStaleData?: boolean
104109
data?: Data
105110
docPermissions: SanitizedDocumentPermissions | undefined
106111
docPreferences: DocumentPreferences
@@ -127,6 +132,11 @@ export type BuildFormStateArgs = {
127132
*/
128133
mockRSCs?: boolean
129134
operation?: 'create' | 'update'
135+
/**
136+
* The original updatedAt timestamp from when the document was initially loaded.
137+
* Used with checkForStaleData to detect if the document has been modified.
138+
*/
139+
originalUpdatedAt?: string
130140
readOnly?: boolean
131141
/**
132142
* If true, will render field components within their state object.

packages/translations/src/clientKeys.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ export const clientTranslationKeys = createClientTranslationKeys([
234234
'general:document',
235235
'general:documentIsTrashed',
236236
'general:documentLocked',
237+
'general:documentModified',
238+
'general:documentOutOfDate',
237239
'general:documents',
238240
'general:duplicate',
239241
'general:duplicateWithoutSaving',
@@ -315,6 +317,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
315317
'general:previous',
316318
'general:reindex',
317319
'general:reindexingAll',
320+
'general:reloadDocument',
318321
'general:remove',
319322
'general:rename',
320323
'general:reset',

packages/translations/src/languages/ar.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ export const arTranslations: DefaultTranslationsObject = {
292292
document: 'وثيقة',
293293
documentIsTrashed: 'تم تحويل {{label}} هذا إلى المهملات وهو للقراءة فقط.',
294294
documentLocked: 'تم قفل المستند',
295+
documentModified: 'تم تعديل المستند',
296+
documentOutOfDate: 'تم تحديث هذا المستند مؤخرًا بواسطة مستخدم آخر. عرضك غير محدث.',
295297
documents: 'وثائق',
296298
duplicate: 'استنساخ',
297299
duplicateWithoutSaving: 'استنساخ بدون حفظ التغييرات',
@@ -380,6 +382,7 @@ export const arTranslations: DefaultTranslationsObject = {
380382
previous: 'سابق',
381383
reindex: 'إعادة الفهرسة',
382384
reindexingAll: 'جاري إعادة فهرسة جميع {{collections}}.',
385+
reloadDocument: 'أعد تحميل الوثيقة',
383386
remove: 'إزالة',
384387
rename: 'إعادة تسمية',
385388
reset: 'إعادة تعيين',

packages/translations/src/languages/az.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,9 @@ export const azTranslations: DefaultTranslationsObject = {
304304
document: 'Sənəd',
305305
documentIsTrashed: 'Bu {{label}} zibil qutusuna atılıb və yalnız oxuna bilər.',
306306
documentLocked: 'Sənəd kilidləndi',
307+
documentModified: 'Sənəd dəyişdirildi',
308+
documentOutOfDate:
309+
'Bu sənəd yeni başqa bir istifadəçi tərəfindən yenilənib. Sizin baxışınız köhnədir.',
307310
documents: 'Sənədlər',
308311
duplicate: 'Dublikat',
309312
duplicateWithoutSaving: 'Dəyişiklikləri saxlamadan dublikatla',
@@ -394,6 +397,7 @@ export const azTranslations: DefaultTranslationsObject = {
394397
previous: 'Əvvəlki',
395398
reindex: 'Yenidən indekslə',
396399
reindexingAll: 'Bütün {{collections}} yenidən indekslənir.',
400+
reloadDocument: 'Sənədə yenidən yükləyin',
397401
remove: 'Sil',
398402
rename: 'Yenidən adlandırın',
399403
reset: 'Yenidən başlat',

packages/translations/src/languages/bg.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ export const bgTranslations: DefaultTranslationsObject = {
301301
document: 'Документ',
302302
documentIsTrashed: 'Този {{label}} е изтрит и е само за четене.',
303303
documentLocked: 'Документът е заключен',
304+
documentModified: 'Модифициран документ',
305+
documentOutOfDate:
306+
'Този документ беше наскоро обновен от друг потребител. Вашият изглед е неактуален.',
304307
documents: 'Документи',
305308
duplicate: 'Дупликирай',
306309
duplicateWithoutSaving: 'Дупликирай без да запазваш промените',
@@ -391,6 +394,7 @@ export const bgTranslations: DefaultTranslationsObject = {
391394
previous: 'Предишен',
392395
reindex: 'Преиндексиране',
393396
reindexingAll: 'Преиндексиране на всички {{collections}}.',
397+
reloadDocument: 'Презареди документ',
394398
remove: 'Премахни',
395399
rename: 'Преименувайте',
396400
reset: 'Нулиране',

packages/translations/src/languages/bnBd.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,9 @@ export const bnBdTranslations: DefaultTranslationsObject = {
306306
document: 'ডকুমেন্ট',
307307
documentIsTrashed: 'এই {{label}} ট্র্যাশ করা হয়েছে এবং এটি শুধুমাত্র পাঠনীয়।',
308308
documentLocked: 'ডকুমেন্ট লক করা হয়েছে',
309+
documentModified: 'নথি পরিবর্তিত হয়েছে',
310+
documentOutOfDate:
311+
'এই নথিটি সাম্প্রতিকভাবে অন্য ব্যবহারকারীর দ্বারা আপডেট করা হয়েছে। আপনার দর্শন আপতিত।',
309312
documents: 'ডকুমেন্টগুলি',
310313
duplicate: 'ডুপ্লিকেট করুন',
311314
duplicateWithoutSaving: 'পরিবর্তনগুলি সংরক্ষণ না করে ডুপ্লিকেট করুন',
@@ -397,6 +400,7 @@ export const bnBdTranslations: DefaultTranslationsObject = {
397400
previous: 'পূর্ববর্তী',
398401
reindex: 'পুনরায় সূচিবদ্ধ করুন',
399402
reindexingAll: 'সমস্ত {{collections}} পুনরায় সূচিবদ্ধ করা হচ্ছে।',
403+
reloadDocument: 'নথি পুনরায় লোড করুন',
400404
remove: 'অপসারণ করুন',
401405
rename: 'নাম পরিবর্তন করুন',
402406
reset: 'রিসেট করুন',

packages/translations/src/languages/bnIn.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ export const bnInTranslations: DefaultTranslationsObject = {
305305
document: 'ডকুমেন্ট',
306306
documentIsTrashed: 'এই {{label}} টি মুছে ফেলা হয়েছে এবং এটি কেবল পড়ার জন্য।',
307307
documentLocked: 'ডকুমেন্ট লক করা হয়েছে',
308+
documentModified: 'নথি পরিবর্তিত হয়েছে',
309+
documentOutOfDate:
310+
'এই নথিটি সম্প্রতি অন্য ব্যবহারকারীর দ্বারা আপডেট করা হয়েছে। আপনার দেখানো ভার্সন আপ টু ডেট নয়।',
308311
documents: 'ডকুমেন্টগুলি',
309312
duplicate: 'ডুপ্লিকেট করুন',
310313
duplicateWithoutSaving: 'পরিবর্তনগুলি সংরক্ষণ না করে ডুপ্লিকেট করুন',
@@ -396,6 +399,7 @@ export const bnInTranslations: DefaultTranslationsObject = {
396399
previous: 'পূর্ববর্তী',
397400
reindex: 'পুনরায় সূচিবদ্ধ করুন',
398401
reindexingAll: 'সমস্ত {{collections}} পুনরায় সূচিবদ্ধ করা হচ্ছে।',
402+
reloadDocument: 'নথি পুনরায় লোড করুন',
399403
remove: 'অপসারণ করুন',
400404
rename: 'নাম পরিবর্তন করুন',
401405
reset: 'রিসেট করুন',

packages/translations/src/languages/ca.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ export const caTranslations: DefaultTranslationsObject = {
303303
document: 'Document',
304304
documentIsTrashed: "Aquesta {{label}} s'ha eliminat i és de només lectura.",
305305
documentLocked: 'Document bloquejat',
306+
documentModified: 'Document modificat',
307+
documentOutOfDate:
308+
'Aquest document ha estat actualitzat recentment per un altre usuari. La seva vista està desactualitzada.',
306309
documents: 'Documents',
307310
duplicate: 'Duplicar',
308311
duplicateWithoutSaving: 'Duplica sense desar',
@@ -394,6 +397,7 @@ export const caTranslations: DefaultTranslationsObject = {
394397
previous: 'Previ',
395398
reindex: 'Reindexa',
396399
reindexingAll: 'Reindexa tots el {{collections}}.',
400+
reloadDocument: 'Recarrega el document',
397401
remove: 'Elimina',
398402
rename: 'Canvia el nom',
399403
reset: 'Restableix',

packages/translations/src/languages/cs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,9 @@ export const csTranslations: DefaultTranslationsObject = {
300300
document: 'Dokument',
301301
documentIsTrashed: 'Tento {{label}} je v koši a je pouze pro čtení.',
302302
documentLocked: 'Dokument je uzamčen',
303+
documentModified: 'Dokument upraven',
304+
documentOutOfDate:
305+
'Tento dokument byl nedávno aktualizován jiným uživatelem. Váš pohled je zastaralý.',
303306
documents: 'Dokumenty',
304307
duplicate: 'Duplikovat',
305308
duplicateWithoutSaving: 'Duplikovat bez uložení změn',
@@ -390,6 +393,7 @@ export const csTranslations: DefaultTranslationsObject = {
390393
previous: 'Předchozí',
391394
reindex: 'Přeindexovat',
392395
reindexingAll: 'Přeindexování všech {{collections}}.',
396+
reloadDocument: 'Obnovit dokument',
393397
remove: 'Odstranit',
394398
rename: 'Přejmenovat',
395399
reset: 'Resetovat',

packages/translations/src/languages/da.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,9 @@ export const daTranslations: DefaultTranslationsObject = {
300300
document: 'Dokument',
301301
documentIsTrashed: 'Denne {{label}} er smidt væk og er kun til læsning.',
302302
documentLocked: 'Dette dokument er låst',
303+
documentModified: 'Dokument ændret',
304+
documentOutOfDate:
305+
'Dette dokument er for nylig blevet opdateret af en anden bruger. Din visning er forældet.',
303306
documents: 'Dokumenter',
304307
duplicate: 'Duplikér',
305308
duplicateWithoutSaving: 'Dupliker uden at gemme ændringer',
@@ -390,6 +393,7 @@ export const daTranslations: DefaultTranslationsObject = {
390393
previous: 'Tidligere',
391394
reindex: 'Genindekser',
392395
reindexingAll: 'Genindekserer alle {{collections}}.',
396+
reloadDocument: 'Genindlæs dokument',
393397
remove: 'Fjern',
394398
rename: 'Omdøb',
395399
reset: 'Nulstil',

0 commit comments

Comments
 (0)