mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
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:
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
+48
@@ -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
|
||||
@@ -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
|
||||
@@ -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({
|
||||
|
||||
@@ -13,6 +13,7 @@ export type ConnectionState = {
|
||||
forceDisconnected: boolean
|
||||
inactiveDisconnect: boolean
|
||||
reconnectAt: number | null
|
||||
forcedDisconnectDelay: number
|
||||
lastConnectionAttempt: number
|
||||
error: '' | ConnectionError
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
|
||||
@@ -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>(
|
||||
() => ({
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user