소스 검색

[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
Alf Eaton 2 년 전
부모
커밋
8dbf2b64f8
21개의 변경된 파일251개의 추가작업 그리고 77개의 파일을 삭제
  1. 3 0
      services/web/frontend/extracted-translations.json
  2. 0 1
      services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx
  3. 2 2
      services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
  4. 19 11
      services/web/frontend/js/features/ide-react/components/modals/force-disconnected.tsx
  5. 15 0
      services/web/frontend/js/features/ide-react/components/modals/modals.tsx
  6. 43 0
      services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx
  7. 22 0
      services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx
  8. 105 0
      services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx
  9. 3 0
      services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx
  10. 1 24
      services/web/frontend/js/features/ide-react/context/modals-context.tsx
  11. 19 6
      services/web/frontend/js/features/ide-react/editor/open-documents.ts
  12. 0 19
      services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts
  13. 1 1
      services/web/frontend/js/features/project-list/components/project-list-root.tsx
  14. 2 2
      services/web/frontend/js/shared/components/system-message.tsx
  15. 4 4
      services/web/frontend/js/shared/components/system-messages.tsx
  16. 4 4
      services/web/frontend/js/shared/components/translation-message.tsx
  17. 4 1
      services/web/frontend/js/shared/context/editor-context.jsx
  18. 1 1
      services/web/frontend/stories/project-list/system-messages.stories.tsx
  19. 2 0
      services/web/locales/en.json
  20. 1 1
      services/web/test/frontend/features/project-list/components/system-messages.test.tsx
  21. 0 0
      services/web/types/system-message.ts

+ 3 - 0
services/web/frontend/extracted-translations.json

@@ -194,6 +194,7 @@
   "conflicting_paths_found": "",
   "congratulations_youve_successfully_join_group": "",
   "connected_users": "",
+  "connection_lost": "",
   "contact_group_admin": "",
   "contact_message_label": "",
   "contact_sales": "",
@@ -1033,6 +1034,7 @@
   "save_or_cancel-save": "",
   "save_x_percent_or_more": "",
   "saving": "",
+  "saving_notification_with_seconds": "",
   "search": "",
   "search_bib_files": "",
   "search_command_find": "",
@@ -1133,6 +1135,7 @@
   "something_went_wrong_rendering_pdf_expected": "",
   "something_went_wrong_server": "",
   "somthing_went_wrong_compiling": "",
+  "sorry_the_connection_to_the_server_is_down": "",
   "sorry_your_table_cant_be_displayed_at_the_moment": "",
   "sort_by": "",
   "sort_by_x": "",

+ 0 - 1
services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx

@@ -5,7 +5,6 @@ import { debugging } from '@/utils/debugging'
 import { Alert } from 'react-bootstrap'
 import useScopeValue from '@/shared/hooks/use-scope-value'
 
-// TODO SavingNotificationController, SystemMessagesController, out-of-sync modal
 export function Alerts() {
   const { t } = useTranslation()
   const {

+ 2 - 2
services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx

@@ -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 { useRegisterUserActivity } from '@/features/ide-react/hooks/use-register-user-activity'
 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() {
   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
   useHasLintingError() // pass editor:lint hasLintingError to the compiler
   useOpenFile() // create ide.binaryFilesManager (TODO: move to the history file restore component)
-  useConnectionState() // show modal when editor is forcefully disconnected
 
   return (
     <>
       <Alerts />
+      <Modals />
       <EditorLeftMenu />
       <MainLayout />
     </>

+ 19 - 11
services/web/frontend/js/features/ide-react/components/modals/lock-editor-message-modal.tsx → services/web/frontend/js/features/ide-react/components/modals/force-disconnected.tsx

@@ -2,20 +2,24 @@ import { useTranslation } from 'react-i18next'
 import { Modal } from 'react-bootstrap'
 import AccessibleModal from '@/shared/components/accessible-modal'
 import { memo, useEffect, useState } from 'react'
+import { useConnectionContext } from '@/features/ide-react/context/connection-context'
 
-export type LockEditorMessageModalProps = {
-  delay: number // In seconds
-  show: boolean
-}
-
-function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) {
+// show modal when editor is forcefully disconnected
+function ForceDisconnected() {
+  const { connectionState } = useConnectionContext()
   const { t } = useTranslation()
   const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(0)
+  const [show, setShow] = useState(false)
 
   useEffect(() => {
-    if (show) {
-      setSecondsUntilRefresh(delay)
+    if (connectionState.forceDisconnected) {
+      setShow(true)
+      setSecondsUntilRefresh(connectionState.forcedDisconnectDelay)
+    }
+  }, [connectionState])
 
+  useEffect(() => {
+    if (show) {
       const timer = window.setInterval(() => {
         setSecondsUntilRefresh(seconds => Math.max(0, seconds - 1))
       }, 1000)
@@ -24,11 +28,15 @@ function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) {
         window.clearInterval(timer)
       }
     }
-  }, [show, delay])
+  }, [show])
+
+  if (!show) {
+    return null
+  }
 
   return (
     <AccessibleModal
-      show={show}
+      show
       // It's not possible to hide this modal, but it's a required prop
       onHide={() => {}}
       className="lock-editor-modal"
@@ -45,4 +53,4 @@ function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) {
   )
 }
 
-export default memo(LockEditorMessageModal)
+export default memo(ForceDisconnected)

+ 15 - 0
services/web/frontend/js/features/ide-react/components/modals/modals.tsx

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

+ 43 - 0
services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx

@@ -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>
+  )
+}

+ 22 - 0
services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx

@@ -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>
+  )
+}

+ 105 - 0
services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx

@@ -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} />}
+    </>
+  )
+}

+ 3 - 0
services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx

@@ -53,6 +53,7 @@ type EditorManager = {
   stopIgnoringExternalUpdates: () => void
   openDocId: (docId: string, options?: OpenDocOptions) => void
   openDoc: (document: Doc, options?: OpenDocOptions) => void
+  openDocs: OpenDocuments
   openInitialDoc: (docId: string) => void
   jumpToLine: (options: GotoLineOptions) => void
 }
@@ -594,6 +595,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
       stopIgnoringExternalUpdates,
       openDocId: openDocWithId,
       openDoc,
+      openDocs,
       openInitialDoc,
       jumpToLine,
     }),
@@ -608,6 +610,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
       stopIgnoringExternalUpdates,
       openDocWithId,
       openDoc,
+      openDocs,
       openInitialDoc,
       jumpToLine,
     ]

+ 1 - 24
services/web/frontend/js/features/ide-react/context/modals-context.tsx

@@ -12,7 +12,6 @@ import GenericMessageModal, {
 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
@@ -23,7 +22,6 @@ type ModalsContextValue = {
   showOutOfSyncModal: (
     editorContent: OutOfSyncModalProps['editorContent']
   ) => void
-  showLockEditorMessageModal: (delay: number) => void
 }
 
 const ModalsContext = createContext<ModalsContextValue | undefined>(undefined)
@@ -39,12 +37,6 @@ export const ModalsContextProvider: FC = ({ children }) => {
     editorContent: '',
   })
 
-  const [shouldShowLockEditorModal, setShouldShowLockEditorModal] =
-    useState(false)
-  const [lockEditorModalData, setLockEditorModalData] = useState({
-    delay: 0,
-  })
-
   const handleHideGenericModal = useCallback(() => {
     setShowGenericModal(false)
   }, [])
@@ -69,24 +61,13 @@ export const ModalsContextProvider: FC = ({ children }) => {
     setShouldShowOutOfSyncModal(true)
   }, [])
 
-  const showLockEditorMessageModal = useCallback((delay: number) => {
-    setLockEditorModalData({ delay })
-    setShouldShowLockEditorModal(true)
-  }, [])
-
   const value = useMemo<ModalsContextValue>(
     () => ({
       showGenericMessageModal,
       genericModalVisible: showGenericModal,
       showOutOfSyncModal,
-      showLockEditorMessageModal,
     }),
-    [
-      showGenericMessageModal,
-      showGenericModal,
-      showOutOfSyncModal,
-      showLockEditorMessageModal,
-    ]
+    [showGenericMessageModal, showGenericModal, showOutOfSyncModal]
   )
 
   return (
@@ -102,10 +83,6 @@ export const ModalsContextProvider: FC = ({ children }) => {
         show={shouldShowOutOfSyncModal}
         onHide={handleHideOutOfSyncModal}
       />
-      <LockEditorMessageModal
-        {...lockEditorModalData}
-        show={shouldShowLockEditorModal}
-      />
     </ModalsContext.Provider>
   )
 }

+ 19 - 6
services/web/frontend/js/features/ide-react/editor/open-documents.ts

@@ -61,15 +61,28 @@ export class OpenDocuments {
     })
   }
 
-  private docsArray() {
-    return Array.from(this.openDocs.values())
-  }
-
   hasUnsavedChanges() {
-    return this.docsArray().some(doc => doc.hasBufferedOps())
+    for (const doc of this.openDocs.values()) {
+      if (doc.hasBufferedOps()) {
+        return true
+      }
+    }
+    return false
   }
 
   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
   }
 }

+ 0 - 19
services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts

@@ -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,
-  ])
-}

+ 1 - 1
services/web/frontend/js/features/project-list/components/project-list-root.tsx

@@ -13,7 +13,7 @@ import ProjectListTable from './table/project-list-table'
 import SurveyWidget from './survey-widget'
 import WelcomeMessage from './welcome-message'
 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 SearchForm from './search-form'
 import ProjectsDropdown from './dropdown/projects-dropdown'

+ 2 - 2
services/web/frontend/js/features/project-list/components/notifications/system-message.tsx → services/web/frontend/js/shared/components/system-message.tsx

@@ -1,5 +1,5 @@
-import Close from '../../../../shared/components/close'
-import usePersistedState from '../../../../shared/hooks/use-persisted-state'
+import Close from './close'
+import usePersistedState from '../hooks/use-persisted-state'
 
 type SystemMessageProps = {
   id: string

+ 4 - 4
services/web/frontend/js/features/project-list/components/notifications/system-messages.tsx → services/web/frontend/js/shared/components/system-messages.tsx

@@ -1,13 +1,13 @@
 import { useEffect } from 'react'
 import SystemMessage from './system-message'
 import TranslationMessage from './translation-message'
-import useAsync from '../../../../shared/hooks/use-async'
-import { getJSON } from '../../../../infrastructure/fetch-json'
-import getMeta from '../../../../utils/meta'
+import useAsync from '../hooks/use-async'
+import { getJSON } from '@/infrastructure/fetch-json'
+import getMeta from '../../utils/meta'
 import {
   SystemMessage as TSystemMessage,
   SuggestedLanguage,
-} from '../../../../../../types/project/dashboard/system-message'
+} from '../../../../types/system-message'
 import { debugConsole } from '@/utils/debugging'
 
 const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000

+ 4 - 4
services/web/frontend/js/features/project-list/components/notifications/translation-message.tsx → services/web/frontend/js/shared/components/translation-message.tsx

@@ -1,8 +1,8 @@
 import { Trans, useTranslation } from 'react-i18next'
-import Close from '../../../../shared/components/close'
-import usePersistedState from '../../../../shared/hooks/use-persisted-state'
-import getMeta from '../../../../utils/meta'
-import { SuggestedLanguage } from '../../../../../../types/project/dashboard/system-message'
+import Close from './close'
+import usePersistedState from '../hooks/use-persisted-state'
+import getMeta from '../../utils/meta'
+import { SuggestedLanguage } from '../../../../types/system-message'
 
 function TranslationMessage() {
   const { t } = useTranslation()

+ 4 - 1
services/web/frontend/js/shared/context/editor-context.jsx

@@ -82,7 +82,8 @@ export function EditorProvider({ children }) {
 
   const [loading] = useScopeValue('state.loading')
   const [projectName, setProjectName] = useScopeValue('project.name')
-  const [permissionsLevel] = useScopeValue('permissionsLevel')
+  const [permissionsLevel, setPermissionsLevel] =
+    useScopeValue('permissionsLevel')
   const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
   const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
 
@@ -164,6 +165,7 @@ export function EditorProvider({ children }) {
       loading,
       renameProject,
       permissionsLevel,
+      setPermissionsLevel,
       isProjectOwner: owner?._id === userId,
       isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
       showSymbolPalette,
@@ -180,6 +182,7 @@ export function EditorProvider({ children }) {
       loading,
       renameProject,
       permissionsLevel,
+      setPermissionsLevel,
       showSymbolPalette,
       toggleSymbolPalette,
       insertSymbol,

+ 1 - 1
services/web/frontend/stories/project-list/system-messages.stories.tsx

@@ -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 { FetchMockStatic } from 'fetch-mock'
 

+ 2 - 0
services/web/locales/en.json

@@ -305,6 +305,7 @@
   "congratulations_youve_successfully_join_group": "Congratulations! You‘ve successfully joined the group subscription.",
   "connected_users": "Connected Users",
   "connecting": "Connecting",
+  "connection_lost": "Connection lost",
   "contact": "Contact",
   "contact_group_admin": "Please contact your group administrator.",
   "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.",
   "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_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_token_expired": "Sorry, your token expired",
   "sort_by": "Sort by",

+ 1 - 1
services/web/test/frontend/features/project-list/components/system-messages.test.tsx

@@ -1,7 +1,7 @@
 import { expect } from 'chai'
 import { render, screen, fireEvent } from '@testing-library/react'
 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 () {
   beforeEach(function () {

+ 0 - 0
services/web/types/project/dashboard/system-message.ts → services/web/types/system-message.ts