mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[ide-react] Notify about unsaved changes (#16163)
* Notify about unsaved changes * Move system message components and types to shared folder * Add system messages component GitOrigin-RevId: ab81a24888847bd9a8a390fd1af6b58f471f7a4b
This commit is contained in:
parent
a074054cc9
commit
8dbf2b64f8
21 changed files with 251 additions and 77 deletions
|
@ -194,6 +194,7 @@
|
||||||
"conflicting_paths_found": "",
|
"conflicting_paths_found": "",
|
||||||
"congratulations_youve_successfully_join_group": "",
|
"congratulations_youve_successfully_join_group": "",
|
||||||
"connected_users": "",
|
"connected_users": "",
|
||||||
|
"connection_lost": "",
|
||||||
"contact_group_admin": "",
|
"contact_group_admin": "",
|
||||||
"contact_message_label": "",
|
"contact_message_label": "",
|
||||||
"contact_sales": "",
|
"contact_sales": "",
|
||||||
|
@ -1033,6 +1034,7 @@
|
||||||
"save_or_cancel-save": "",
|
"save_or_cancel-save": "",
|
||||||
"save_x_percent_or_more": "",
|
"save_x_percent_or_more": "",
|
||||||
"saving": "",
|
"saving": "",
|
||||||
|
"saving_notification_with_seconds": "",
|
||||||
"search": "",
|
"search": "",
|
||||||
"search_bib_files": "",
|
"search_bib_files": "",
|
||||||
"search_command_find": "",
|
"search_command_find": "",
|
||||||
|
@ -1133,6 +1135,7 @@
|
||||||
"something_went_wrong_rendering_pdf_expected": "",
|
"something_went_wrong_rendering_pdf_expected": "",
|
||||||
"something_went_wrong_server": "",
|
"something_went_wrong_server": "",
|
||||||
"somthing_went_wrong_compiling": "",
|
"somthing_went_wrong_compiling": "",
|
||||||
|
"sorry_the_connection_to_the_server_is_down": "",
|
||||||
"sorry_your_table_cant_be_displayed_at_the_moment": "",
|
"sorry_your_table_cant_be_displayed_at_the_moment": "",
|
||||||
"sort_by": "",
|
"sort_by": "",
|
||||||
"sort_by_x": "",
|
"sort_by_x": "",
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { debugging } from '@/utils/debugging'
|
||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||||
|
|
||||||
// TODO SavingNotificationController, SystemMessagesController, out-of-sync modal
|
|
||||||
export function Alerts() {
|
export function Alerts() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useOpenFile } from '@/features/ide-react/hooks/use-open-file'
|
||||||
import { useEditingSessionHeartbeat } from '@/features/ide-react/hooks/use-editing-session-heartbeat'
|
import { useEditingSessionHeartbeat } from '@/features/ide-react/hooks/use-editing-session-heartbeat'
|
||||||
import { useRegisterUserActivity } from '@/features/ide-react/hooks/use-register-user-activity'
|
import { useRegisterUserActivity } from '@/features/ide-react/hooks/use-register-user-activity'
|
||||||
import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-error'
|
import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-error'
|
||||||
import { useConnectionState } from '@/features/ide-react/hooks/use-connection-state'
|
import { Modals } from '@/features/ide-react/components/modals/modals'
|
||||||
|
|
||||||
export default function IdePage() {
|
export default function IdePage() {
|
||||||
useLayoutEventTracking() // sent event when the layout changes
|
useLayoutEventTracking() // sent event when the layout changes
|
||||||
|
@ -16,11 +16,11 @@ export default function IdePage() {
|
||||||
useRegisterUserActivity() // record activity and ensure connection when user is active
|
useRegisterUserActivity() // record activity and ensure connection when user is active
|
||||||
useHasLintingError() // pass editor:lint hasLintingError to the compiler
|
useHasLintingError() // pass editor:lint hasLintingError to the compiler
|
||||||
useOpenFile() // create ide.binaryFilesManager (TODO: move to the history file restore component)
|
useOpenFile() // create ide.binaryFilesManager (TODO: move to the history file restore component)
|
||||||
useConnectionState() // show modal when editor is forcefully disconnected
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Alerts />
|
<Alerts />
|
||||||
|
<Modals />
|
||||||
<EditorLeftMenu />
|
<EditorLeftMenu />
|
||||||
<MainLayout />
|
<MainLayout />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -2,20 +2,24 @@ import { useTranslation } from 'react-i18next'
|
||||||
import { Modal } from 'react-bootstrap'
|
import { Modal } from 'react-bootstrap'
|
||||||
import AccessibleModal from '@/shared/components/accessible-modal'
|
import AccessibleModal from '@/shared/components/accessible-modal'
|
||||||
import { memo, useEffect, useState } from 'react'
|
import { memo, useEffect, useState } from 'react'
|
||||||
|
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||||
|
|
||||||
export type LockEditorMessageModalProps = {
|
// show modal when editor is forcefully disconnected
|
||||||
delay: number // In seconds
|
function ForceDisconnected() {
|
||||||
show: boolean
|
const { connectionState } = useConnectionContext()
|
||||||
}
|
|
||||||
|
|
||||||
function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(0)
|
const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(0)
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (connectionState.forceDisconnected) {
|
||||||
|
setShow(true)
|
||||||
|
setSecondsUntilRefresh(connectionState.forcedDisconnectDelay)
|
||||||
|
}
|
||||||
|
}, [connectionState])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) {
|
if (show) {
|
||||||
setSecondsUntilRefresh(delay)
|
|
||||||
|
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
setSecondsUntilRefresh(seconds => Math.max(0, seconds - 1))
|
setSecondsUntilRefresh(seconds => Math.max(0, seconds - 1))
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
@ -24,11 +28,15 @@ function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) {
|
||||||
window.clearInterval(timer)
|
window.clearInterval(timer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [show, delay])
|
}, [show])
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleModal
|
<AccessibleModal
|
||||||
show={show}
|
show
|
||||||
// It's not possible to hide this modal, but it's a required prop
|
// It's not possible to hide this modal, but it's a required prop
|
||||||
onHide={() => {}}
|
onHide={() => {}}
|
||||||
className="lock-editor-modal"
|
className="lock-editor-modal"
|
||||||
|
@ -45,4 +53,4 @@ function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(LockEditorMessageModal)
|
export default memo(ForceDisconnected)
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { memo } from 'react'
|
||||||
|
import ForceDisconnected from '@/features/ide-react/components/modals/force-disconnected'
|
||||||
|
import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsaved-docs'
|
||||||
|
import SystemMessages from '@/shared/components/system-messages'
|
||||||
|
|
||||||
|
export const Modals = memo(() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ForceDisconnected />
|
||||||
|
<UnsavedDocs />
|
||||||
|
<SystemMessages />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Modals.displayName = 'Modals'
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { FC, useMemo } from 'react'
|
||||||
|
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||||
|
import { Alert } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export const UnsavedDocsAlert: FC<{ unsavedDocs: Map<string, number> }> = ({
|
||||||
|
unsavedDocs,
|
||||||
|
}) => (
|
||||||
|
<div className="global-alerts">
|
||||||
|
{[...unsavedDocs.entries()].map(
|
||||||
|
([docId, seconds]) =>
|
||||||
|
seconds > 8 && (
|
||||||
|
<UnsavedDocAlert key={docId} docId={docId} seconds={seconds} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const UnsavedDocAlert: FC<{ docId: string; seconds: number }> = ({
|
||||||
|
docId,
|
||||||
|
seconds,
|
||||||
|
}) => {
|
||||||
|
const { pathInFolder, findEntityByPath } = useFileTreePathContext()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const doc = useMemo(() => {
|
||||||
|
const path = pathInFolder(docId)
|
||||||
|
return path ? findEntityByPath(path) : null
|
||||||
|
}, [docId, findEntityByPath, pathInFolder])
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert bsStyle="warning" bsSize="small">
|
||||||
|
{t('saving_notification_with_seconds', {
|
||||||
|
docname: doc.entity.name,
|
||||||
|
seconds,
|
||||||
|
})}
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { Modal } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import AccessibleModal from '@/shared/components/accessible-modal'
|
||||||
|
|
||||||
|
export const UnsavedDocsLockedModal: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleModal
|
||||||
|
onHide={() => {}} // It's not possible to hide this modal, but it's a required prop
|
||||||
|
className="lock-editor-modal"
|
||||||
|
backdrop={false}
|
||||||
|
keyboard={false}
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>{t('connection_lost')}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>{t('sorry_the_connection_to_the_server_is_down')}</Modal.Body>
|
||||||
|
</AccessibleModal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||||
|
import { useEditorContext } from '@/shared/context/editor-context'
|
||||||
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { PermissionsLevel } from '@/features/ide-react/types/permissions'
|
||||||
|
import { UnsavedDocsLockedModal } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal'
|
||||||
|
import { UnsavedDocsAlert } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-alert'
|
||||||
|
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||||
|
|
||||||
|
const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
|
||||||
|
|
||||||
|
export const UnsavedDocs: FC = () => {
|
||||||
|
const { openDocs } = useEditorManagerContext()
|
||||||
|
const { permissionsLevel, setPermissionsLevel } = useEditorContext()
|
||||||
|
const [isLocked, setIsLocked] = useState(false)
|
||||||
|
const [unsavedDocs, setUnsavedDocs] = useState(new Map<string, number>())
|
||||||
|
|
||||||
|
// always contains the latest value
|
||||||
|
const previousUnsavedDocsRef = useRef(unsavedDocs)
|
||||||
|
useEffect(() => {
|
||||||
|
previousUnsavedDocsRef.current = unsavedDocs
|
||||||
|
}, [unsavedDocs])
|
||||||
|
|
||||||
|
// always contains the latest value
|
||||||
|
const permissionsLevelRef = useRef(permissionsLevel)
|
||||||
|
useEffect(() => {
|
||||||
|
permissionsLevelRef.current = permissionsLevel
|
||||||
|
}, [permissionsLevel])
|
||||||
|
|
||||||
|
// warn if the window is being closed with unsaved changes
|
||||||
|
useEventListener(
|
||||||
|
'beforeunload',
|
||||||
|
useCallback(
|
||||||
|
event => {
|
||||||
|
if (openDocs.hasUnsavedChanges()) {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[openDocs]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// keep track of which docs are currently unsaved, and how long they've been unsaved for
|
||||||
|
// NOTE: openDocs should never change, so it's safe to use as a dependency here
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
const unsavedDocs = new Map()
|
||||||
|
|
||||||
|
const unsavedDocIds = openDocs.unsavedDocIds()
|
||||||
|
|
||||||
|
for (const docId of unsavedDocIds) {
|
||||||
|
const unsavedSeconds =
|
||||||
|
(previousUnsavedDocsRef.current.get(docId) ?? 0) + 1
|
||||||
|
unsavedDocs.set(docId, unsavedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid setting the unsavedDocs state to a new empty Map every second
|
||||||
|
if (unsavedDocs.size > 0 || previousUnsavedDocsRef.current.size > 0) {
|
||||||
|
previousUnsavedDocsRef.current = unsavedDocs
|
||||||
|
setUnsavedDocs(unsavedDocs)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [openDocs])
|
||||||
|
|
||||||
|
const maxUnsavedSeconds = Math.max(0, ...unsavedDocs.values())
|
||||||
|
|
||||||
|
// lock the editor if at least one doc has been unsaved for too long
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLocked(maxUnsavedSeconds > MAX_UNSAVED_SECONDS)
|
||||||
|
}, [maxUnsavedSeconds])
|
||||||
|
|
||||||
|
// display a modal and set the permissions level to readOnly if docs have been unsaved for too long
|
||||||
|
const originalPermissionsLevelRef = useRef<PermissionsLevel | null>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLocked) {
|
||||||
|
originalPermissionsLevelRef.current = permissionsLevelRef.current
|
||||||
|
// TODO: what if the real permissions level changes in the meantime?
|
||||||
|
// TODO: perhaps the "locked" state should be stored in the editor context instead?
|
||||||
|
setPermissionsLevel('readOnly')
|
||||||
|
setIsLocked(true)
|
||||||
|
} else {
|
||||||
|
if (originalPermissionsLevelRef.current) {
|
||||||
|
setPermissionsLevel(originalPermissionsLevelRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isLocked, setPermissionsLevel])
|
||||||
|
|
||||||
|
// remove the modal (and unlock the page) if the connection has been re-established and all the docs have been saved
|
||||||
|
useEffect(() => {
|
||||||
|
if (unsavedDocs.size === 0 && permissionsLevelRef.current === 'readOnly') {
|
||||||
|
setIsLocked(false)
|
||||||
|
}
|
||||||
|
}, [unsavedDocs])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLocked && <UnsavedDocsLockedModal />}
|
||||||
|
{unsavedDocs.size > 0 && <UnsavedDocsAlert unsavedDocs={unsavedDocs} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -53,6 +53,7 @@ type EditorManager = {
|
||||||
stopIgnoringExternalUpdates: () => void
|
stopIgnoringExternalUpdates: () => void
|
||||||
openDocId: (docId: string, options?: OpenDocOptions) => void
|
openDocId: (docId: string, options?: OpenDocOptions) => void
|
||||||
openDoc: (document: Doc, options?: OpenDocOptions) => void
|
openDoc: (document: Doc, options?: OpenDocOptions) => void
|
||||||
|
openDocs: OpenDocuments
|
||||||
openInitialDoc: (docId: string) => void
|
openInitialDoc: (docId: string) => void
|
||||||
jumpToLine: (options: GotoLineOptions) => void
|
jumpToLine: (options: GotoLineOptions) => void
|
||||||
}
|
}
|
||||||
|
@ -594,6 +595,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
||||||
stopIgnoringExternalUpdates,
|
stopIgnoringExternalUpdates,
|
||||||
openDocId: openDocWithId,
|
openDocId: openDocWithId,
|
||||||
openDoc,
|
openDoc,
|
||||||
|
openDocs,
|
||||||
openInitialDoc,
|
openInitialDoc,
|
||||||
jumpToLine,
|
jumpToLine,
|
||||||
}),
|
}),
|
||||||
|
@ -608,6 +610,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
||||||
stopIgnoringExternalUpdates,
|
stopIgnoringExternalUpdates,
|
||||||
openDocWithId,
|
openDocWithId,
|
||||||
openDoc,
|
openDoc,
|
||||||
|
openDocs,
|
||||||
openInitialDoc,
|
openInitialDoc,
|
||||||
jumpToLine,
|
jumpToLine,
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,7 +12,6 @@ import GenericMessageModal, {
|
||||||
import OutOfSyncModal, {
|
import OutOfSyncModal, {
|
||||||
OutOfSyncModalProps,
|
OutOfSyncModalProps,
|
||||||
} from '@/features/ide-react/components/modals/out-of-sync-modal'
|
} from '@/features/ide-react/components/modals/out-of-sync-modal'
|
||||||
import LockEditorMessageModal from '@/features/ide-react/components/modals/lock-editor-message-modal'
|
|
||||||
|
|
||||||
type ModalsContextValue = {
|
type ModalsContextValue = {
|
||||||
genericModalVisible: boolean
|
genericModalVisible: boolean
|
||||||
|
@ -23,7 +22,6 @@ type ModalsContextValue = {
|
||||||
showOutOfSyncModal: (
|
showOutOfSyncModal: (
|
||||||
editorContent: OutOfSyncModalProps['editorContent']
|
editorContent: OutOfSyncModalProps['editorContent']
|
||||||
) => void
|
) => void
|
||||||
showLockEditorMessageModal: (delay: number) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalsContext = createContext<ModalsContextValue | undefined>(undefined)
|
const ModalsContext = createContext<ModalsContextValue | undefined>(undefined)
|
||||||
|
@ -39,12 +37,6 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
editorContent: '',
|
editorContent: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const [shouldShowLockEditorModal, setShouldShowLockEditorModal] =
|
|
||||||
useState(false)
|
|
||||||
const [lockEditorModalData, setLockEditorModalData] = useState({
|
|
||||||
delay: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleHideGenericModal = useCallback(() => {
|
const handleHideGenericModal = useCallback(() => {
|
||||||
setShowGenericModal(false)
|
setShowGenericModal(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -69,24 +61,13 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
setShouldShowOutOfSyncModal(true)
|
setShouldShowOutOfSyncModal(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const showLockEditorMessageModal = useCallback((delay: number) => {
|
|
||||||
setLockEditorModalData({ delay })
|
|
||||||
setShouldShowLockEditorModal(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const value = useMemo<ModalsContextValue>(
|
const value = useMemo<ModalsContextValue>(
|
||||||
() => ({
|
() => ({
|
||||||
showGenericMessageModal,
|
showGenericMessageModal,
|
||||||
genericModalVisible: showGenericModal,
|
genericModalVisible: showGenericModal,
|
||||||
showOutOfSyncModal,
|
showOutOfSyncModal,
|
||||||
showLockEditorMessageModal,
|
|
||||||
}),
|
}),
|
||||||
[
|
[showGenericMessageModal, showGenericModal, showOutOfSyncModal]
|
||||||
showGenericMessageModal,
|
|
||||||
showGenericModal,
|
|
||||||
showOutOfSyncModal,
|
|
||||||
showLockEditorMessageModal,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -102,10 +83,6 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
show={shouldShowOutOfSyncModal}
|
show={shouldShowOutOfSyncModal}
|
||||||
onHide={handleHideOutOfSyncModal}
|
onHide={handleHideOutOfSyncModal}
|
||||||
/>
|
/>
|
||||||
<LockEditorMessageModal
|
|
||||||
{...lockEditorModalData}
|
|
||||||
show={shouldShowLockEditorModal}
|
|
||||||
/>
|
|
||||||
</ModalsContext.Provider>
|
</ModalsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,15 +61,28 @@ export class OpenDocuments {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private docsArray() {
|
|
||||||
return Array.from(this.openDocs.values())
|
|
||||||
}
|
|
||||||
|
|
||||||
hasUnsavedChanges() {
|
hasUnsavedChanges() {
|
||||||
return this.docsArray().some(doc => doc.hasBufferedOps())
|
for (const doc of this.openDocs.values()) {
|
||||||
|
if (doc.hasBufferedOps()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
flushAll() {
|
flushAll() {
|
||||||
return this.docsArray().map(doc => doc.flush())
|
for (const doc of this.openDocs.values()) {
|
||||||
|
doc.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsavedDocIds() {
|
||||||
|
const ids = []
|
||||||
|
for (const [docId, doc] of this.openDocs) {
|
||||||
|
if (!doc.pollSavedStatus()) {
|
||||||
|
ids.push(docId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
|
||||||
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
|
||||||
|
|
||||||
export const useConnectionState = () => {
|
|
||||||
const { connectionState } = useConnectionContext()
|
|
||||||
const { showLockEditorMessageModal } = useModalsContext()
|
|
||||||
|
|
||||||
// Show modal when editor is forcefully disconnected
|
|
||||||
useEffect(() => {
|
|
||||||
if (connectionState.forceDisconnected) {
|
|
||||||
showLockEditorMessageModal(connectionState.forcedDisconnectDelay)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
connectionState.forceDisconnected,
|
|
||||||
connectionState.forcedDisconnectDelay,
|
|
||||||
showLockEditorMessageModal,
|
|
||||||
])
|
|
||||||
}
|
|
|
@ -13,7 +13,7 @@ import ProjectListTable from './table/project-list-table'
|
||||||
import SurveyWidget from './survey-widget'
|
import SurveyWidget from './survey-widget'
|
||||||
import WelcomeMessage from './welcome-message'
|
import WelcomeMessage from './welcome-message'
|
||||||
import LoadingBranded from '../../../shared/components/loading-branded'
|
import LoadingBranded from '../../../shared/components/loading-branded'
|
||||||
import SystemMessages from './notifications/system-messages'
|
import SystemMessages from '../../../shared/components/system-messages'
|
||||||
import UserNotifications from './notifications/user-notifications'
|
import UserNotifications from './notifications/user-notifications'
|
||||||
import SearchForm from './search-form'
|
import SearchForm from './search-form'
|
||||||
import ProjectsDropdown from './dropdown/projects-dropdown'
|
import ProjectsDropdown from './dropdown/projects-dropdown'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Close from '../../../../shared/components/close'
|
import Close from './close'
|
||||||
import usePersistedState from '../../../../shared/hooks/use-persisted-state'
|
import usePersistedState from '../hooks/use-persisted-state'
|
||||||
|
|
||||||
type SystemMessageProps = {
|
type SystemMessageProps = {
|
||||||
id: string
|
id: string
|
|
@ -1,13 +1,13 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import SystemMessage from './system-message'
|
import SystemMessage from './system-message'
|
||||||
import TranslationMessage from './translation-message'
|
import TranslationMessage from './translation-message'
|
||||||
import useAsync from '../../../../shared/hooks/use-async'
|
import useAsync from '../hooks/use-async'
|
||||||
import { getJSON } from '../../../../infrastructure/fetch-json'
|
import { getJSON } from '@/infrastructure/fetch-json'
|
||||||
import getMeta from '../../../../utils/meta'
|
import getMeta from '../../utils/meta'
|
||||||
import {
|
import {
|
||||||
SystemMessage as TSystemMessage,
|
SystemMessage as TSystemMessage,
|
||||||
SuggestedLanguage,
|
SuggestedLanguage,
|
||||||
} from '../../../../../../types/project/dashboard/system-message'
|
} from '../../../../types/system-message'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
|
||||||
const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
|
const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
|
|
@ -1,8 +1,8 @@
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import Close from '../../../../shared/components/close'
|
import Close from './close'
|
||||||
import usePersistedState from '../../../../shared/hooks/use-persisted-state'
|
import usePersistedState from '../hooks/use-persisted-state'
|
||||||
import getMeta from '../../../../utils/meta'
|
import getMeta from '../../utils/meta'
|
||||||
import { SuggestedLanguage } from '../../../../../../types/project/dashboard/system-message'
|
import { SuggestedLanguage } from '../../../../types/system-message'
|
||||||
|
|
||||||
function TranslationMessage() {
|
function TranslationMessage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
|
@ -82,7 +82,8 @@ export function EditorProvider({ children }) {
|
||||||
|
|
||||||
const [loading] = useScopeValue('state.loading')
|
const [loading] = useScopeValue('state.loading')
|
||||||
const [projectName, setProjectName] = useScopeValue('project.name')
|
const [projectName, setProjectName] = useScopeValue('project.name')
|
||||||
const [permissionsLevel] = useScopeValue('permissionsLevel')
|
const [permissionsLevel, setPermissionsLevel] =
|
||||||
|
useScopeValue('permissionsLevel')
|
||||||
const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
|
const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
|
||||||
const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
|
const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
|
||||||
|
|
||||||
|
@ -164,6 +165,7 @@ export function EditorProvider({ children }) {
|
||||||
loading,
|
loading,
|
||||||
renameProject,
|
renameProject,
|
||||||
permissionsLevel,
|
permissionsLevel,
|
||||||
|
setPermissionsLevel,
|
||||||
isProjectOwner: owner?._id === userId,
|
isProjectOwner: owner?._id === userId,
|
||||||
isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
|
isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
|
||||||
showSymbolPalette,
|
showSymbolPalette,
|
||||||
|
@ -180,6 +182,7 @@ export function EditorProvider({ children }) {
|
||||||
loading,
|
loading,
|
||||||
renameProject,
|
renameProject,
|
||||||
permissionsLevel,
|
permissionsLevel,
|
||||||
|
setPermissionsLevel,
|
||||||
showSymbolPalette,
|
showSymbolPalette,
|
||||||
toggleSymbolPalette,
|
toggleSymbolPalette,
|
||||||
insertSymbol,
|
insertSymbol,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SystemMessages from '../../js/features/project-list/components/notifications/system-messages'
|
import SystemMessages from '@/shared/components/system-messages'
|
||||||
import useFetchMock from '../hooks/use-fetch-mock'
|
import useFetchMock from '../hooks/use-fetch-mock'
|
||||||
import { FetchMockStatic } from 'fetch-mock'
|
import { FetchMockStatic } from 'fetch-mock'
|
||||||
|
|
||||||
|
|
|
@ -305,6 +305,7 @@
|
||||||
"congratulations_youve_successfully_join_group": "Congratulations! You‘ve successfully joined the group subscription.",
|
"congratulations_youve_successfully_join_group": "Congratulations! You‘ve successfully joined the group subscription.",
|
||||||
"connected_users": "Connected Users",
|
"connected_users": "Connected Users",
|
||||||
"connecting": "Connecting",
|
"connecting": "Connecting",
|
||||||
|
"connection_lost": "Connection lost",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"contact_group_admin": "Please contact your group administrator.",
|
"contact_group_admin": "Please contact your group administrator.",
|
||||||
"contact_message_label": "Message",
|
"contact_message_label": "Message",
|
||||||
|
@ -1688,6 +1689,7 @@
|
||||||
"somthing_went_wrong_compiling": "Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.",
|
"somthing_went_wrong_compiling": "Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.",
|
||||||
"sorry_detected_sales_restricted_region": "Sorry, we’ve detected that you are in a region from which we cannot presently accept payments. If you think you’ve received this message in error, please <a href=\"__link__\">contact us</a> with details of your location, and we will look into this for you. We apologize for the inconvenience.",
|
"sorry_detected_sales_restricted_region": "Sorry, we’ve detected that you are in a region from which we cannot presently accept payments. If you think you’ve received this message in error, please <a href=\"__link__\">contact us</a> with details of your location, and we will look into this for you. We apologize for the inconvenience.",
|
||||||
"sorry_something_went_wrong_opening_the_document_please_try_again": "Sorry, an unexpected error occurred when trying to open this content on Overleaf. Please try again.",
|
"sorry_something_went_wrong_opening_the_document_please_try_again": "Sorry, an unexpected error occurred when trying to open this content on Overleaf. Please try again.",
|
||||||
|
"sorry_the_connection_to_the_server_is_down": "Sorry, the connection to the server is down.",
|
||||||
"sorry_your_table_cant_be_displayed_at_the_moment": "Sorry, your table can’t be displayed at the moment.",
|
"sorry_your_table_cant_be_displayed_at_the_moment": "Sorry, your table can’t be displayed at the moment.",
|
||||||
"sorry_your_token_expired": "Sorry, your token expired",
|
"sorry_your_token_expired": "Sorry, your token expired",
|
||||||
"sort_by": "Sort by",
|
"sort_by": "Sort by",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { render, screen, fireEvent } from '@testing-library/react'
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
import fetchMock from 'fetch-mock'
|
import fetchMock from 'fetch-mock'
|
||||||
import SystemMessages from '../../../../../frontend/js/features/project-list/components/notifications/system-messages'
|
import SystemMessages from '@/shared/components/system-messages'
|
||||||
|
|
||||||
describe('<SystemMessages />', function () {
|
describe('<SystemMessages />', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
|
Loading…
Reference in a new issue