From d1691345c5ea05472f6ec6c31abcb4eefa4c6877 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Fri, 31 Jul 2026 14:58:58 +0100 Subject: [PATCH] Merge pull request #36074 from overleaf/mj-reload-after-failed-catch-up [web] Reload after failed intermittent-connection catchup GitOrigin-RevId: c9a6315ecb6ebe957ad85c05ebc84bb8165881ab --- .../modals/unable-to-sync-modal.tsx | 13 +- .../context/editor-manager-context.tsx | 4 +- .../ide-react/context/modals-context.tsx | 19 ++- .../ide-react/create-ide-event-emitter.ts | 1 + ...ditor-manager-context-out-of-sync.spec.tsx | 111 ++++++++++++++++++ 5 files changed, 143 insertions(+), 5 deletions(-) diff --git a/services/web/frontend/js/features/ide-react/components/modals/unable-to-sync-modal.tsx b/services/web/frontend/js/features/ide-react/components/modals/unable-to-sync-modal.tsx index 541ba5989d..d840e2af5a 100644 --- a/services/web/frontend/js/features/ide-react/components/modals/unable-to-sync-modal.tsx +++ b/services/web/frontend/js/features/ide-react/components/modals/unable-to-sync-modal.tsx @@ -14,6 +14,7 @@ import { uploadBatch } from '@/infrastructure/batch-file-uploader' import { debugConsole } from '@/utils/debugging' import Notification from '@/shared/components/notification' import { downloadFileContent } from '@/utils/download-file' +import { useLocation } from '@/shared/hooks/use-location' export type UnableToSyncModalProps = { baseContent: string @@ -22,6 +23,7 @@ export type UnableToSyncModalProps = { rootFolderId: string | undefined show: boolean onHide: () => void + reloadAfterClose?: boolean } // Generates the offline copy filename with a timestamp to avoid duplicates. @@ -52,11 +54,20 @@ function UnableToSyncModal({ docName, rootFolderId, show, - onHide, + onHide: onHideProp, + reloadAfterClose = false, }: UnableToSyncModalProps) { const { t } = useTranslation() const [saving, setSaving] = useState(false) const [saveError, setSaveError] = useState(false) + const location = useLocation() + + const onHide = useCallback(() => { + onHideProp() + if (reloadAfterClose) { + location.reload() + } + }, [onHideProp, reloadAfterClose, location]) const handleDownload = useCallback(() => { downloadFileContent(targetContent, docName ?? 'document.txt') diff --git a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx index 28d0c99a69..4647f33fef 100644 --- a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx @@ -606,6 +606,7 @@ export const EditorManagerProvider: FC = ({ editorContent: editorContent || '', baseContent: backupRecord.snapshot, docName: document.docName, + reloadAfterClose: true, }) OfflineDocBackup.remove(projectId, document.doc_id) } else { @@ -644,13 +645,14 @@ export const EditorManagerProvider: FC = ({ useEffect(() => { const handleUnableToSync = ({ - detail: [{ editorContent, baseContent, docName }], + detail: [{ editorContent, baseContent, docName, reloadAfterClose }], }: CustomEvent) => { showUnableToSyncModal({ baseContent, targetContent: editorContent, docName, rootFolderId: fileTreeDataRef.current?._id, + reloadAfterClose, }) } diff --git a/services/web/frontend/js/features/ide-react/context/modals-context.tsx b/services/web/frontend/js/features/ide-react/context/modals-context.tsx index 290c406669..9db3eb3f31 100644 --- a/services/web/frontend/js/features/ide-react/context/modals-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/modals-context.tsx @@ -32,7 +32,11 @@ type ModalsContextValue = { showUnableToSyncModal: ( data: Pick< UnableToSyncModalProps, - 'baseContent' | 'targetContent' | 'docName' | 'rootFolderId' + | 'baseContent' + | 'targetContent' + | 'docName' + | 'rootFolderId' + | 'reloadAfterClose' > ) => void } @@ -64,13 +68,18 @@ export const ModalsContextProvider: FC = ({ const [unableToSyncModalData, setUnableToSyncModalData] = useState< Pick< UnableToSyncModalProps, - 'baseContent' | 'targetContent' | 'docName' | 'rootFolderId' + | 'baseContent' + | 'targetContent' + | 'docName' + | 'rootFolderId' + | 'reloadAfterClose' > >({ baseContent: '', targetContent: '', docName: null, rootFolderId: undefined, + reloadAfterClose: undefined, }) const handleHideGenericModal = useCallback(() => { @@ -122,7 +131,11 @@ export const ModalsContextProvider: FC = ({ ( data: Pick< UnableToSyncModalProps, - 'baseContent' | 'targetContent' | 'docName' | 'rootFolderId' + | 'baseContent' + | 'targetContent' + | 'docName' + | 'rootFolderId' + | 'reloadAfterClose' > ) => { setUnableToSyncModalData(data) diff --git a/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts b/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts index 2e5e0379c6..07e04d0306 100644 --- a/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts +++ b/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts @@ -28,6 +28,7 @@ export type IdeEvents = { editorContent: string baseContent: string docName: string + reloadAfterClose?: boolean }, ] 'ide:offlineChangesSynced': [{ docId: string }] diff --git a/services/web/test/frontend/features/ide-react/context/editor-manager-context-out-of-sync.spec.tsx b/services/web/test/frontend/features/ide-react/context/editor-manager-context-out-of-sync.spec.tsx index 50aa3d526c..55fe4acfbc 100644 --- a/services/web/test/frontend/features/ide-react/context/editor-manager-context-out-of-sync.spec.tsx +++ b/services/web/test/frontend/features/ide-react/context/editor-manager-context-out-of-sync.spec.tsx @@ -9,6 +9,7 @@ import type { SocketDebuggingInfo } from '@/features/ide-react/connection/types/ import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter' import { OfflineDocBackup } from '@/features/ide-react/editor/offline-doc-backup' import { GlobalToasts } from '@/features/ide-react/components/global-toasts' +import { location } from '@/shared/components/location' import { EditorProviders, makeEditorOpenDocProvider, @@ -177,6 +178,31 @@ describe('EditorManagerProvider docError sync modals', function () { }) }) + it('reloads the page when the backup-path modal is closed', function () { + cy.then(() => { + setSplitTest(true) + plantBackup(CURRENT_DOC_ID) + cy.stub(location, 'reload').as('reload') + }) + + mount() + + cy.then(() => { + currentDoc.trigger( + 'error', + new Error('forced'), + {}, + 'offline edits content' + ) + }) + + cy.findByRole('dialog').within(() => { + cy.findByRole('button', { name: 'Discard changes' }).click() + }) + + cy.get('@reload').should('have.been.calledOnce') + }) + it('shows nothing and keeps the backup when the failure happens while offline', function () { let key: string cy.then(() => { @@ -279,6 +305,91 @@ describe('EditorManagerProvider docError sync modals', function () { }) }) + it('does not reload when the recovery event omits reloadAfterClose', function () { + let capturedEmitter: IdeEventEmitter | null = null + + cy.then(() => { + cy.stub(location, 'reload').as('reload') + }) + + cy.mount( + + { + capturedEmitter = emitter + }} + /> + + ) + + cy.then(() => { + capturedEmitter!.emit('ide:unableToSyncOfflineChanges', { + docId: CURRENT_DOC_ID, + editorContent: 'recovered content', + baseContent: 'original content', + docName: 'recovered.tex', + }) + }) + + cy.findByRole('dialog').within(() => { + cy.findByRole('button', { name: 'Discard changes' }).click() + }) + + cy.get('@reload').should('not.have.been.called') + }) + + it('reloads when the recovery event sets reloadAfterClose', function () { + let capturedEmitter: IdeEventEmitter | null = null + + cy.then(() => { + cy.stub(location, 'reload').as('reload') + }) + + cy.mount( + + { + capturedEmitter = emitter + }} + /> + + ) + + cy.then(() => { + capturedEmitter!.emit('ide:unableToSyncOfflineChanges', { + docId: CURRENT_DOC_ID, + editorContent: 'recovered content', + baseContent: 'original content', + docName: 'recovered.tex', + reloadAfterClose: true, + }) + }) + + cy.findByRole('dialog').within(() => { + cy.findByRole('button', { name: 'Discard changes' }).click() + }) + + cy.get('@reload').should('have.been.calledOnce') + }) + it('shows the success toast on the ide:offlineChangesSynced event', function () { let capturedEmitter: IdeEventEmitter | null = null