From e0a2af4bf6623fdde1326ac96f21837850663ec2 Mon Sep 17 00:00:00 2001 From: Davinder Singh Date: Fri, 24 Jul 2026 10:34:14 +0100 Subject: [PATCH] Merge pull request #35725 from overleaf/ds-unable-to-sync-modal [WEB] adds unable-to-sync modal for intermittent connection improvements GitOrigin-RevId: 4cd85cf742cb3f36cc0c97f83bdeab09bb6f7c53 --- .../web/frontend/extracted-translations.json | 3 ++ .../modals/unable-to-sync-modal.tsx | 53 +++++++++++++++++++ .../ide-react/context/modals-context.tsx | 36 +++++++++++++ .../toolbar/download-file-button.tsx | 11 +--- .../web/frontend/js/utils/download-file.ts | 11 ++++ services/web/locales/en.json | 3 ++ 6 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 services/web/frontend/js/features/ide-react/components/modals/unable-to-sync-modal.tsx create mode 100644 services/web/frontend/js/utils/download-file.ts diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index c8e179436e..bf354e3d23 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -543,6 +543,7 @@ "download_as_source_zip": "", "download_csv": "", "download_file": "", + "download_local_version": "", "download_metadata": "", "download_pdf": "", "download_zip_file": "", @@ -1358,6 +1359,7 @@ "of": "", "off": "", "official": "", + "offline_edits_couldnt_be_synced_detail": "", "ok": "", "ok_continue_to_project": "", "ok_join_project": "", @@ -2551,6 +2553,7 @@ "your_name_and_email_address_will_be_visible_to_project_editors": "", "your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors": "", "your_new_plan": "", + "your_offline_edits_couldnt_be_synced": "", "your_password_was_detected": "", "your_plan": "", "your_plan_is_changing_at_term_end": "", 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 new file mode 100644 index 0000000000..d50969405e --- /dev/null +++ b/services/web/frontend/js/features/ide-react/components/modals/unable-to-sync-modal.tsx @@ -0,0 +1,53 @@ +import { useTranslation } from 'react-i18next' +import { memo, useCallback } from 'react' +import OLButton from '@/shared/components/ol/ol-button' +import { + OLModal, + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/shared/components/ol/ol-modal' +import { downloadFileContent } from '@/utils/download-file' + +export type UnableToSyncModalProps = { + editorContent: string + docName: string | null + show: boolean + onHide: () => void +} + +function UnableToSyncModal({ + editorContent, + docName, + show, + onHide, +}: UnableToSyncModalProps) { + const { t } = useTranslation() + + const handleDownload = useCallback(() => { + downloadFileContent(editorContent, docName ?? 'document.txt') + }, [editorContent, docName]) + + return ( + + + {t('your_offline_edits_couldnt_be_synced')} + + {t('offline_edits_couldnt_be_synced_detail')} + + + {t('download_local_version')} + + + + ) +} + +export default memo(UnableToSyncModal) 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 a00b676262..a0c036a17b 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 @@ -12,6 +12,9 @@ import GenericMessageModal, { import OutOfSyncModal, { OutOfSyncModalProps, } from '@/features/ide-react/components/modals/out-of-sync-modal' +import UnableToSyncModal, { + UnableToSyncModalProps, +} from '@/features/ide-react/components/modals/unable-to-sync-modal' import GenericConfirmModal, { GenericConfirmModalOwnProps, } from '../components/modals/generic-confirm-modal' @@ -26,6 +29,10 @@ type ModalsContextValue = { showOutOfSyncModal: ( editorContent: OutOfSyncModalProps['editorContent'] ) => void + showUnableToSyncModal: ( + editorContent: UnableToSyncModalProps['editorContent'], + docName: UnableToSyncModalProps['docName'] + ) => void } const ModalsContext = createContext(undefined) @@ -50,6 +57,16 @@ export const ModalsContextProvider: FC = ({ editorContent: '', }) + const [shouldShowUnableToSyncModal, setShouldShowUnableToSyncModal] = + useState(false) + const [unableToSyncModalData, setUnableToSyncModalData] = useState<{ + editorContent: string + docName: string | null + }>({ + editorContent: '', + docName: null, + }) + const handleHideGenericModal = useCallback(() => { setShowGenericModal(false) }, []) @@ -91,18 +108,32 @@ export const ModalsContextProvider: FC = ({ setShouldShowOutOfSyncModal(true) }, []) + const handleHideUnableToSyncModal = useCallback(() => { + setShouldShowUnableToSyncModal(false) + }, []) + + const showUnableToSyncModal = useCallback( + (editorContent: string, docName: string | null) => { + setUnableToSyncModalData({ editorContent, docName }) + setShouldShowUnableToSyncModal(true) + }, + [] + ) + const value = useMemo( () => ({ showGenericMessageModal, showGenericConfirmModal, genericModalVisible: showGenericModal, showOutOfSyncModal, + showUnableToSyncModal, }), [ showGenericMessageModal, showGenericConfirmModal, showGenericModal, showOutOfSyncModal, + showUnableToSyncModal, ] ) @@ -125,6 +156,11 @@ export const ModalsContextProvider: FC = ({ show={shouldShowOutOfSyncModal} onHide={handleHideOutOfSyncModal} /> + ) } diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/download-file-button.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/download-file-button.tsx index 4a687ebb22..0aacbbd8e4 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/download-file-button.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/download-file-button.tsx @@ -3,6 +3,7 @@ import { useCodeMirrorViewContext } from '../codemirror-context' import { useCallback } from 'react' import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { useTranslation } from 'react-i18next' +import { downloadFileContent } from '@/utils/download-file' const DownloadFileButton = () => { const view = useCodeMirrorViewContext() @@ -11,15 +12,7 @@ const DownloadFileButton = () => { const downloadFile = useCallback(() => { const doc = view.state.doc.toString() - const blob = new Blob([doc], { type: 'application/octet-stream' }) - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = openDocName ?? 'document.txt' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - setTimeout(() => URL.revokeObjectURL(url), 0) + downloadFileContent(doc, openDocName ?? 'document.txt') }, [view, openDocName]) return ( diff --git a/services/web/frontend/js/utils/download-file.ts b/services/web/frontend/js/utils/download-file.ts new file mode 100644 index 0000000000..4b2fce760b --- /dev/null +++ b/services/web/frontend/js/utils/download-file.ts @@ -0,0 +1,11 @@ +export function downloadFileContent(content: string, filename: string) { + const blob = new Blob([content], { type: 'application/octet-stream' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + setTimeout(() => URL.revokeObjectURL(url), 0) +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 7867d27ac3..ef89d48a46 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -701,6 +701,7 @@ "download_as_source_zip": "Download as source (.zip)", "download_csv": "Download CSV", "download_file": "Download file", + "download_local_version": "Download local version", "download_metadata": "Download Overleaf metadata", "download_pdf": "Download PDF", "download_zip_file": "Download .zip file", @@ -1764,6 +1765,7 @@ "october": "October", "off": "Off", "official": "Official", + "offline_edits_couldnt_be_synced_detail": "This file changed while you were offline, so we’ve loaded the latest version. Download your local copy below to add back any changes you made.", "ok": "OK", "ok_continue_to_project": "OK, continue to project", "ok_join_project": "OK, join project", @@ -3214,6 +3216,7 @@ "your_name_and_email_address_will_be_visible_to_project_editors": "Your name and email address will be visible to project editors.", "your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors": "Your name and email address will be visible to the project owner and other editors.", "your_new_plan": "Your new plan", + "your_offline_edits_couldnt_be_synced": "Your offline edits couldn’t be synced", "your_password_has_been_reset": "Your password has been reset", "your_password_has_been_successfully_changed": "Your password has been successfully changed", "your_password_was_detected": "Your password is on a <0>public list of known compromised passwords. Keep your account safe by changing your password now.",