diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 965be282ba..948258b0f1 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -466,6 +466,7 @@ "helps_us_tailor_your_experience": "", "hide_configuration": "", "hide_document_preamble": "", + "hide_local_file_contents": "", "hide_outline": "", "history": "", "history_add_label": "", @@ -790,6 +791,8 @@ "organize_projects": "", "other_logs_and_files": "", "other_output_files": "", + "out_of_sync": "", + "out_of_sync_detail": "", "output_file": "", "overall_theme": "", "overleaf": "", @@ -843,6 +846,7 @@ "please_select_a_project": "", "please_select_an_output_file": "", "please_set_main_file": "", + "please_wait": "", "plus_additional_collaborators_document_history_track_changes_and_more": "", "plus_more": "", "plus_upgraded_accounts_receive": "", @@ -875,6 +879,8 @@ "project_not_linked_to_github": "", "project_ownership_transfer_confirmation_1": "", "project_ownership_transfer_confirmation_2": "", + "project_renamed_or_deleted": "", + "project_renamed_or_deleted_detail": "", "project_synced_with_git_repo_at": "", "project_synchronisation": "", "project_timed_out_enable_stop_on_first_error": "", @@ -936,6 +942,7 @@ "reject": "", "reject_all": "", "relink_your_account": "", + "reload_editor": "", "remote_service_error": "", "remove": "", "remove_collaborator": "", @@ -1068,6 +1075,7 @@ "show_in_code": "", "show_in_pdf": "", "show_less": "", + "show_local_file_contents": "", "show_outline": "", "show_x_more": "", "show_x_more_projects": "", @@ -1362,6 +1370,7 @@ "welcome_to_sl": "", "were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "", "were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "", + "were_performing_maintenance": "", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "", "what_does_this_mean": "", diff --git a/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx index c0fb7394ae..c4ed9d8f23 100644 --- a/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx +++ b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx @@ -21,7 +21,7 @@ export function Alerts() { return (
{connectionState.forceDisconnected ? ( - + {t('disconnected')} ) : null} diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx index d739d37046..37139c2ef8 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx @@ -10,6 +10,7 @@ import EditorNavigationToolbar from '@/features/ide-react/components/editor-navi import ChatPane from '@/features/chat/components/chat-pane' import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking' import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners' +import { useModalsContext } from '@/features/ide-react/context/modals-context' import { useOpenFile } from '@/features/ide-react/hooks/use-open-file' // This is filled with placeholder content while the real content is migrated @@ -25,7 +26,19 @@ export default function IdePage() { useOpenFile() const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20) - const { registerUserActivity } = useConnectionContext() + const { connectionState, registerUserActivity } = useConnectionContext() + const { showLockEditorMessageModal } = useModalsContext() + + // Show modal when editor is forcefully disconnected + useEffect(() => { + if (connectionState.forceDisconnected) { + showLockEditorMessageModal(connectionState.forcedDisconnectDelay) + } + }, [ + connectionState.forceDisconnected, + connectionState.forcedDisconnectDelay, + showLockEditorMessageModal, + ]) // Inform the connection manager when the user is active const listener = useCallback( diff --git a/services/web/frontend/js/features/ide-react/components/modals/lock-editor-message-modal.tsx b/services/web/frontend/js/features/ide-react/components/modals/lock-editor-message-modal.tsx new file mode 100644 index 0000000000..6dbee9fe72 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/components/modals/lock-editor-message-modal.tsx @@ -0,0 +1,48 @@ +import { useTranslation } from 'react-i18next' +import { Modal } from 'react-bootstrap' +import AccessibleModal from '@/shared/components/accessible-modal' +import { useEffect, useState } from 'react' + +export type LockEditorMessageModalProps = { + delay: number // In seconds + show: boolean +} + +function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) { + const { t } = useTranslation() + const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(0) + + useEffect(() => { + if (show) { + setSecondsUntilRefresh(delay) + + const timer = window.setInterval(() => { + setSecondsUntilRefresh(seconds => Math.max(0, seconds - 1)) + }, 1000) + + return () => { + window.clearInterval(timer) + } + } + }, [show, delay]) + + return ( + {}} + className="lock-editor-modal" + backdrop={false} + keyboard={false} + > + + {t('please_wait')} + + + {t('were_performing_maintenance', { seconds: secondsUntilRefresh })} + + + ) +} + +export default LockEditorMessageModal diff --git a/services/web/frontend/js/features/ide-react/components/modals/out-of-sync-modal.tsx b/services/web/frontend/js/features/ide-react/components/modals/out-of-sync-modal.tsx new file mode 100644 index 0000000000..f009df1c86 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/components/modals/out-of-sync-modal.tsx @@ -0,0 +1,81 @@ +import { Trans, useTranslation } from 'react-i18next' +import { Button, Modal } from 'react-bootstrap' +import AccessibleModal from '@/shared/components/accessible-modal' +import { useState } from 'react' +import { useLocation } from '@/shared/hooks/use-location' + +export type OutOfSyncModalProps = { + editorContent: string + show: boolean + onHide: () => void +} + +function OutOfSyncModal({ editorContent, show, onHide }: OutOfSyncModalProps) { + const { t } = useTranslation() + const location = useLocation() + const [editorContentShown, setEditorContentShown] = useState(false) + const editorContentRows = (editorContent.match(/\n/g)?.length || 0) + 1 + + // Reload the page to avoid staying in an inconsistent state. + // https://github.com/overleaf/issues/issues/3694 + function done() { + onHide() + location.reload() + } + + return ( + + + {t('out_of_sync')} + + + , + // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key + , + ]} + /> + + + + {editorContentShown ? ( +
+