diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index f7bb9de8b..df1585997 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -1420,6 +1420,7 @@ describe('DocumentDetailComponent', () => { it('should queue incoming update while network is active and flush after', () => { initNormally() const loadSpy = jest.spyOn(component as any, 'loadDocument') + const toastSpy = jest.spyOn(toastService, 'showInfo') component.networkActive = true ;(component as any).handleIncomingDocumentUpdated({ @@ -1433,6 +1434,28 @@ describe('DocumentDetailComponent', () => { ;(component as any).flushPendingIncomingUpdate() expect(loadSpy).toHaveBeenCalledWith(component.documentId, true) + expect(toastSpy).toHaveBeenCalledWith( + 'Document reloaded with latest changes.' + ) + }) + + it('should ignore queued incoming update matching local save modified', () => { + initNormally() + const loadSpy = jest.spyOn(component as any, 'loadDocument') + const toastSpy = jest.spyOn(toastService, 'showInfo') + + component.networkActive = true + ;(component as any).lastLocalSaveModified = '2026-02-17T00:00:00+00:00' + ;(component as any).handleIncomingDocumentUpdated({ + document_id: component.documentId, + modified: '2026-02-17T00:00:00+00:00', + }) + + component.networkActive = false + ;(component as any).flushPendingIncomingUpdate() + + expect(loadSpy).not.toHaveBeenCalled() + expect(toastSpy).not.toHaveBeenCalled() }) it('should change preview element by render type', () => { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index e771e9d6d..1917a6707 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -277,6 +277,7 @@ export class DocumentDetailComponent docChangeNotifier: Subject = new Subject() private incomingUpdateModal: NgbModalRef private pendingIncomingUpdate: IncomingDocumentUpdate + private lastLocalSaveModified: string | null = null requiresPassword: boolean = false password: string @@ -539,11 +540,18 @@ export class DocumentDetailComponent this.handleIncomingDocumentUpdated(pendingUpdate) } + private getModifiedRawValue(modified: string | Date): string | null { + if (!modified) return null + if (typeof modified === 'string') return modified + return modified.toISOString() + } + private loadDocument(documentId: number, forceRemote: boolean = false): void { let redirectedToRoot = false this.closeIncomingUpdateModal() this.pendingIncomingUpdate = null this.selectedVersionId = documentId + this.lastLocalSaveModified = null this.previewUrl = this.documentsService.getPreviewUrl( this.selectedVersionId ) @@ -671,6 +679,18 @@ export class DocumentDetailComponent this.pendingIncomingUpdate = data return } + // If modified timestamp of the incoming update is the same as the last local save, + // we assume this update is from our own save and dont notify + const incomingModified = this.getModifiedRawValue(data.modified) + if ( + incomingModified && + this.lastLocalSaveModified && + incomingModified === this.lastLocalSaveModified + ) { + this.lastLocalSaveModified = null + return + } + this.lastLocalSaveModified = null if (this.openDocumentService.isDirty(this.document)) { this.showIncomingUpdateModal(data.modified) @@ -1188,6 +1208,9 @@ export class DocumentDetailComponent .subscribe({ next: (docValues) => { this.closeIncomingUpdateModal() + this.lastLocalSaveModified = this.getModifiedRawValue( + docValues.modified + ) // in case data changed while saving eg removing inbox_tags this.documentForm.patchValue(docValues) const newValues = Object.assign({}, this.documentForm.value) @@ -1214,6 +1237,7 @@ export class DocumentDetailComponent }, error: (error) => { this.networkActive = false + this.lastLocalSaveModified = null const canEdit = this.permissionsService.currentUserHasObjectPermissions( PermissionAction.Change, @@ -1274,6 +1298,7 @@ export class DocumentDetailComponent this.error = null this.networkActive = false this.pendingIncomingUpdate = null + this.lastLocalSaveModified = null if (closeResult && updateResult && nextDocId) { this.router.navigate(['documents', nextDocId]) this.titleInput?.focus() @@ -1281,6 +1306,7 @@ export class DocumentDetailComponent }, error: (error) => { this.networkActive = false + this.lastLocalSaveModified = null this.error = error.error this.toastService.showError($localize`Error saving document`, error) this.flushPendingIncomingUpdate()