Merge pull request #15592 from overleaf/td-ide-page-more-modals

React IDE page: add out-of-sync modal and project deleted modal

GitOrigin-RevId: 6081a40c3009e8ebc5b3f9752c4059af3d52966d
This commit is contained in:
Tim Down 2023-11-06 13:11:06 +00:00 committed by Copybot
parent ecf7befe88
commit c3afce73c1
13 changed files with 257 additions and 25 deletions

View file

@ -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": "",

View file

@ -21,7 +21,7 @@ export function Alerts() {
return (
<div className="global-alerts">
{connectionState.forceDisconnected ? (
<Alert bsStyle="warning" className="small">
<Alert bsStyle="danger" className="small">
<strong>{t('disconnected')}</strong>
</Alert>
) : null}

View file

@ -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(

View file

@ -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 (
<AccessibleModal
show={show}
// It's not possible to hide this modal, but it's a required prop
onHide={() => {}}
className="lock-editor-modal"
backdrop={false}
keyboard={false}
>
<Modal.Header>
<Modal.Title>{t('please_wait')}</Modal.Title>
</Modal.Header>
<Modal.Body>
{t('were_performing_maintenance', { seconds: secondsUntilRefresh })}
</Modal.Body>
</AccessibleModal>
)
}
export default LockEditorMessageModal

View file

@ -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 (
<AccessibleModal
show={show}
onHide={done}
className="out-of-sync-modal"
backdrop={false}
keyboard={false}
>
<Modal.Header closeButton>
<Modal.Title>{t('out_of_sync')}</Modal.Title>
</Modal.Header>
<Modal.Body className="modal-body-share">
<Trans
i18nKey="out_of_sync_detail"
components={[
// eslint-disable-next-line react/jsx-key
<br />,
// eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
<a
target="_blank"
rel="noopener noreferrer"
href="/learn/Kb/Editor_out_of_sync_problems"
/>,
]}
/>
</Modal.Body>
<Modal.Body>
<Button
bsStyle="info"
onClick={() => setEditorContentShown(shown => !shown)}
>
{editorContentShown
? t('hide_local_file_contents')
: t('show_local_file_contents')}
</Button>
{editorContentShown ? (
<div className="text-preview">
<textarea
className="scroll-container"
readOnly
rows={editorContentRows}
value={editorContent}
/>
</div>
) : null}
</Modal.Body>
<Modal.Footer>
<Button bsStyle="info" onClick={done}>
{t('reload_editor')}
</Button>
</Modal.Footer>
</AccessibleModal>
)
}
export default OutOfSyncModal

View file

@ -24,6 +24,7 @@ const initialState: ConnectionState = {
inactiveDisconnect: false,
lastConnectionAttempt: 0,
reconnectAt: null,
forcedDisconnectDelay: 0,
error: '',
}
@ -181,9 +182,10 @@ export class ConnectionManager extends Emitter<Events> {
this.changeState({
...this.state,
forceDisconnected: true,
forcedDisconnectDelay: delay,
error,
})
setTimeout(() => this.disconnect(), delay * 1000)
setTimeout(() => this.disconnect(), 1000)
}
private onJoinProjectResponse({

View file

@ -13,6 +13,7 @@ export type ConnectionState = {
forceDisconnected: boolean
inactiveDisconnect: boolean
reconnectAt: number | null
forcedDisconnectDelay: number
lastConnectionAttempt: number
error: '' | ConnectionError
}

View file

@ -11,6 +11,7 @@ import { ConnectionState } from '../connection/types/connection-state'
import { ConnectionManager } from '@/features/ide-react/connection/connection-manager'
import { Socket } from '@/features/ide-react/connection/types/socket'
import { secondsUntil } from '@/features/ide-react/connection/utils'
import { useLocation } from '@/shared/hooks/use-location'
type ConnectionContextValue = {
socket: Socket
@ -28,6 +29,8 @@ const ConnectionContext = createContext<ConnectionContextValue | undefined>(
)
export const ConnectionProvider: FC = ({ children }) => {
const location = useLocation()
const [connectionManager] = useState(() => new ConnectionManager())
const [connectionState, setConnectionState] = useState(
connectionManager.state
@ -69,6 +72,24 @@ export const ConnectionProvider: FC = ({ children }) => {
connectionManager.disconnect()
}, [connectionManager])
// Reload the page on force disconnect. Doing this in React-land means that we
// can use useLocation(), which provides mockable location methods
useEffect(() => {
if (connectionState.forceDisconnected) {
const timer = window.setTimeout(
() => location.reload(),
connectionState.forcedDisconnectDelay * 1000
)
return () => {
window.clearTimeout(timer)
}
}
}, [
connectionState.forceDisconnected,
connectionState.forcedDisconnectDelay,
location,
])
const value = useMemo<ConnectionContextValue>(
() => ({
socket: connectionManager.socket,

View file

@ -115,7 +115,8 @@ export const EditorManagerProvider: FC = ({ children }) => {
const { reportError, eventEmitter, eventLog } = useIdeReactContext()
const { socket, disconnect } = useConnectionContext()
const { view, setView } = useLayoutContext()
const { showGenericMessageModal, genericModalVisible } = useModalsContext()
const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
useModalsContext()
const [showSymbolPalette, setShowSymbolPalette] = useScopeValue<boolean>(
'editor.showSymbolPalette'
@ -550,13 +551,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
// Tell the user about the error state.
setIsInErrorState(true)
// TODO: MIGRATION: Show out-of-sync modal
// this.ide.showOutOfSyncModal(
// 'Out of sync',
// "Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a target='_blank' rel='noopener noreferrer' href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>",
// editorContent
// )
showOutOfSyncModal(editorContent || '')
// Do not forceReopen the document.
return
@ -581,6 +576,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
reportError,
setIsInErrorState,
showGenericMessageModal,
showOutOfSyncModal,
t,
])

View file

@ -104,9 +104,9 @@ export const IdeReactProvider: FC = ({ children }) => {
user_id: getMeta('ol-user_id'),
project_id: projectId,
// @ts-ignore
client_id: socket.socket.sessionid,
client_id: socket.socket?.sessionid,
// @ts-ignore
transport: socket.socket.transport,
transport: socket.socket?.transport?.name,
client_now: new Date(),
}

View file

@ -20,6 +20,8 @@ import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
import useEventListener from '@/shared/hooks/use-event-listener'
import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
import { Project } from '../../../../../types/project'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
import { useTranslation } from 'react-i18next'
type DocumentMetadata = {
labels: string[]
@ -45,12 +47,14 @@ const MetadataContext = createContext<MetadataContextValue | undefined>(
)
export const MetadataProvider: FC = ({ children }) => {
const { t } = useTranslation()
const ide = useIdeContext()
const { eventEmitter, projectId } = useIdeReactContext()
const { socket } = useConnectionContext()
const { onlineUsersCount } = useOnlineUsersContext()
const { permissionsLevel } = useEditorContext()
const { currentDocument } = useEditorManagerContext()
const { showGenericMessageModal } = useModalsContext()
const [documents, setDocuments] = useState<DocumentsMetadata>({})
@ -180,17 +184,10 @@ export const MetadataProvider: FC = ({ children }) => {
useEffect(() => {
const handleProjectJoined = ({ project }: { project: Project }) => {
if (project.deletedByExternalDataSource) {
// TODO: MIGRATION: Show generic message modal here
/*
ide.showGenericMessageModal(
'Project Renamed or Deleted',
`\
This project has either been renamed or deleted by an external data source such as Dropbox.
We don't want to delete your data on Overleaf, so this project still contains your history and collaborators.
If the project has been renamed please look in your project list for a new project under the new name.\
`
showGenericMessageModal(
t('project_renamed_or_deleted'),
t('project_renamed_or_deleted_detail')
)
*/
}
window.setTimeout(() => {
if (permissionsLevel !== 'readOnly') {
@ -204,7 +201,13 @@ If the project has been renamed please look in your project list for a new proje
return () => {
eventEmitter.off('project:joined', handleProjectJoined)
}
}, [eventEmitter, loadProjectMetaFromServer, permissionsLevel])
}, [
eventEmitter,
loadProjectMetaFromServer,
permissionsLevel,
showGenericMessageModal,
t,
])
const value = useMemo<MetadataContextValue>(
() => ({

View file

@ -9,6 +9,10 @@ import {
import GenericMessageModal, {
GenericMessageModalOwnProps,
} from '@/features/ide-react/components/modals/generic-message-modal'
import OutOfSyncModal, {
OutOfSyncModalProps,
} from '@/features/ide-react/components/modals/out-of-sync-modal'
import LockEditorMessageModal from '@/features/ide-react/components/modals/lock-editor-message-modal'
type ModalsContextValue = {
genericModalVisible: boolean
@ -16,6 +20,10 @@ type ModalsContextValue = {
title: GenericMessageModalOwnProps['title'],
message: GenericMessageModalOwnProps['message']
) => void
showOutOfSyncModal: (
editorContent: OutOfSyncModalProps['editorContent']
) => void
showLockEditorMessageModal: (delay: number) => void
}
const ModalsContext = createContext<ModalsContextValue | undefined>(undefined)
@ -25,6 +33,18 @@ export const ModalsContextProvider: FC = ({ children }) => {
const [genericMessageModalData, setGenericMessageModalData] =
useState<GenericMessageModalOwnProps>({ title: '', message: '' })
const [shouldShowOutOfSyncModal, setShouldShowOutOfSyncModal] =
useState(false)
const [outOfSyncModalData, setOutOfSyncModalData] = useState({
editorContent: '',
})
const [shouldShowLockEditorModal, setShouldShowLockEditorModal] =
useState(false)
const [lockEditorModalData, setLockEditorModalData] = useState({
delay: 0,
})
const handleHideGenericModal = useCallback(() => {
setShowGenericModal(false)
}, [])
@ -40,12 +60,33 @@ export const ModalsContextProvider: FC = ({ children }) => {
[]
)
const handleHideOutOfSyncModal = useCallback(() => {
setShouldShowOutOfSyncModal(false)
}, [])
const showOutOfSyncModal = useCallback((editorContent: string) => {
setOutOfSyncModalData({ editorContent })
setShouldShowOutOfSyncModal(true)
}, [])
const showLockEditorMessageModal = useCallback((delay: number) => {
setLockEditorModalData({ delay })
setShouldShowLockEditorModal(true)
}, [])
const value = useMemo<ModalsContextValue>(
() => ({
showGenericMessageModal,
genericModalVisible: showGenericModal,
showOutOfSyncModal,
showLockEditorMessageModal,
}),
[showGenericMessageModal, showGenericModal]
[
showGenericMessageModal,
showGenericModal,
showOutOfSyncModal,
showLockEditorMessageModal,
]
)
return (
@ -56,6 +97,15 @@ export const ModalsContextProvider: FC = ({ children }) => {
onHide={handleHideGenericModal}
{...genericMessageModalData}
/>
<OutOfSyncModal
{...outOfSyncModalData}
show={shouldShowOutOfSyncModal}
onHide={handleHideOutOfSyncModal}
/>
<LockEditorMessageModal
{...lockEditorModalData}
show={shouldShowLockEditorModal}
/>
</ModalsContext.Provider>
)
}

View file

@ -740,6 +740,7 @@
"helps_us_tailor_your_experience": "This helps us tailor your Overleaf experience.",
"hide_configuration": "Hide configuration",
"hide_document_preamble": "Hide document preamble",
"hide_local_file_contents": "Hide Local File Contents",
"hide_outline": "Hide File outline",
"history": "History",
"history_add_label": "Add label",
@ -1227,6 +1228,8 @@
"other_output_files": "Download other output files",
"other_sessions": "Other Sessions",
"our_values": "Our values",
"out_of_sync": "Out of sync",
"out_of_sync_detail": "Sorry, this file has gone out of sync and we need to do a full refresh.<0 /><1>Please see this help guide for more information</1>",
"output_file": "Output file",
"over": "over",
"over_x_templates_easy_getting_started": "There are thousands of __templates__ in our template gallery, so its really easy to get started, whether youre writing a journal article, thesis, CV or something else.",
@ -1309,6 +1312,7 @@
"please_select_an_output_file": "Please Select an Output File",
"please_set_a_password": "Please set a password",
"please_set_main_file": "Please choose the main file for this project in the project menu. ",
"please_wait": "Please wait",
"plus_additional_collaborators_document_history_track_changes_and_more": "(plus additional collaborators, document history, track changes, and more).",
"plus_more": "plus more",
"plus_upgraded_accounts_receive": "Plus with an upgraded account you get",
@ -1360,6 +1364,8 @@
"project_owner_plus_10": "Project author + 10",
"project_ownership_transfer_confirmation_1": "Are you sure you want to make <0>__user__</0> the owner of <1>__project__</1>?",
"project_ownership_transfer_confirmation_2": "This action cannot be undone. The new owner will be notified and will be able to change project access settings (including removing your own access).",
"project_renamed_or_deleted": "Project Renamed or Deleted",
"project_renamed_or_deleted_detail": "This project has either been renamed or deleted by an external data source such as Dropbox. We dont want to delete your data on Overleaf, so this project still contains your history and collaborators. If the project has been renamed please look in your project list for a new project under the new name.",
"project_synced_with_git_repo_at": "This project is synced with the GitHub repository at",
"project_synchronisation": "Project Synchronisation",
"project_timed_out_enable_stop_on_first_error": "<0>Enable “Stop on first error”</0> to help you find and fix errors right away.",
@ -1625,6 +1631,7 @@
"show_in_code": "Show in code",
"show_in_pdf": "Show in PDF",
"show_less": "show less",
"show_local_file_contents": "Show Local File Contents",
"show_outline": "Show File outline",
"show_x_more": "Show __x__ more",
"show_x_more_projects": "Show __x__ more projects",
@ -2027,6 +2034,7 @@
"welcome_to_sl": "Welcome to __appName__!",
"were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "Were in the process of <0>reducing the compile timeout limit</0> on our free plan, which may affect this project in future.",
"were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "Were in the process of <0>reducing the compile timeout limit</0> on our free plan, which may affect your project in future.",
"were_performing_maintenance": "Were performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected this project.",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected your project.",
"what_does_this_mean": "What does this mean?",