Merge pull request #36074 from overleaf/mj-reload-after-failed-catch-up

[web] Reload after failed intermittent-connection catchup

GitOrigin-RevId: c9a6315ecb6ebe957ad85c05ebc84bb8165881ab
This commit is contained in:
Mathias Jakobsen
2026-09-17 11:44:33 +00:00
committed by Copybot
parent 35ba44f22b
commit d1691345c5
5 changed files with 143 additions and 5 deletions
@@ -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')
@@ -606,6 +606,7 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
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<React.PropsWithChildren> = ({
useEffect(() => {
const handleUnableToSync = ({
detail: [{ editorContent, baseContent, docName }],
detail: [{ editorContent, baseContent, docName, reloadAfterClose }],
}: CustomEvent<IdeEvents['ide:unableToSyncOfflineChanges']>) => {
showUnableToSyncModal({
baseContent,
targetContent: editorContent,
docName,
rootFolderId: fileTreeDataRef.current?._id,
reloadAfterClose,
})
}
@@ -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<React.PropsWithChildren> = ({
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<React.PropsWithChildren> = ({
(
data: Pick<
UnableToSyncModalProps,
'baseContent' | 'targetContent' | 'docName' | 'rootFolderId'
| 'baseContent'
| 'targetContent'
| 'docName'
| 'rootFolderId'
| 'reloadAfterClose'
>
) => {
setUnableToSyncModalData(data)
@@ -28,6 +28,7 @@ export type IdeEvents = {
editorContent: string
baseContent: string
docName: string
reloadAfterClose?: boolean
},
]
'ide:offlineChangesSynced': [{ docId: string }]
@@ -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(
<EditorProviders
projectId={PROJECT_ID}
providers={{
EditorOpenDocProvider: makeEditorOpenDocProvider({
currentDocumentId: CURRENT_DOC_ID as any,
openDocName: currentDoc.docName,
currentDocument: currentDoc as any,
}),
}}
>
<CaptureEventEmitter
onReady={emitter => {
capturedEmitter = emitter
}}
/>
</EditorProviders>
)
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(
<EditorProviders
projectId={PROJECT_ID}
providers={{
EditorOpenDocProvider: makeEditorOpenDocProvider({
currentDocumentId: CURRENT_DOC_ID as any,
openDocName: currentDoc.docName,
currentDocument: currentDoc as any,
}),
}}
>
<CaptureEventEmitter
onReady={emitter => {
capturedEmitter = emitter
}}
/>
</EditorProviders>
)
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