From 4cb530e54db30e2db33a85ed8ca3f88de836fb2b Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Thu, 14 Dec 2023 11:06:36 +0000 Subject: [PATCH] [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 --- .../web/frontend/extracted-translations.json | 3 + .../ide-react/components/alerts/alerts.tsx | 1 - .../ide-react/components/layout/ide-page.tsx | 4 +- ...ssage-modal.tsx => force-disconnected.tsx} | 30 +++-- .../ide-react/components/modals/modals.tsx | 15 +++ .../unsaved-docs/unsaved-docs-alert.tsx | 43 +++++++ .../unsaved-docs-locked-modal.tsx | 22 ++++ .../components/unsaved-docs/unsaved-docs.tsx | 105 ++++++++++++++++++ .../context/editor-manager-context.tsx | 3 + .../ide-react/context/modals-context.tsx | 25 +---- .../ide-react/editor/open-documents.ts | 25 ++++- .../ide-react/hooks/use-connection-state.ts | 19 ---- .../components/project-list-root.tsx | 2 +- .../components}/system-message.tsx | 4 +- .../components}/system-messages.tsx | 8 +- .../components}/translation-message.tsx | 8 +- .../js/shared/context/editor-context.jsx | 5 +- .../project-list/system-messages.stories.tsx | 2 +- services/web/locales/en.json | 2 + .../components/system-messages.test.tsx | 2 +- .../{project/dashboard => }/system-message.ts | 0 21 files changed, 251 insertions(+), 77 deletions(-) rename services/web/frontend/js/features/ide-react/components/modals/{lock-editor-message-modal.tsx => force-disconnected.tsx} (64%) create mode 100644 services/web/frontend/js/features/ide-react/components/modals/modals.tsx create mode 100644 services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx create mode 100644 services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx create mode 100644 services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx delete mode 100644 services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts rename services/web/frontend/js/{features/project-list/components/notifications => shared/components}/system-message.tsx (77%) rename services/web/frontend/js/{features/project-list/components/notifications => shared/components}/system-messages.tsx (84%) rename services/web/frontend/js/{features/project-list/components/notifications => shared/components}/translation-message.tsx (80%) rename services/web/types/{project/dashboard => }/system-message.ts (100%) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index ca3a3eea75..1a519ea2c7 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", 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 c4ed9d8f23..d011e1efd3 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 @@ -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 { 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 b2e2a64dea..91bd7f92e1 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 @@ -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 ( <> + 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/force-disconnected.tsx similarity index 64% rename from services/web/frontend/js/features/ide-react/components/modals/lock-editor-message-modal.tsx rename to services/web/frontend/js/features/ide-react/components/modals/force-disconnected.tsx index 1d0c0e4860..a1fdf24107 100644 --- 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/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 (connectionState.forceDisconnected) { + setShow(true) + setSecondsUntilRefresh(connectionState.forcedDisconnectDelay) + } + }, [connectionState]) useEffect(() => { if (show) { - setSecondsUntilRefresh(delay) - 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 ( {}} className="lock-editor-modal" @@ -45,4 +53,4 @@ function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) { ) } -export default memo(LockEditorMessageModal) +export default memo(ForceDisconnected) diff --git a/services/web/frontend/js/features/ide-react/components/modals/modals.tsx b/services/web/frontend/js/features/ide-react/components/modals/modals.tsx new file mode 100644 index 0000000000..ed031dcf1d --- /dev/null +++ b/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 ( + <> + + + + + ) +}) +Modals.displayName = 'Modals' diff --git a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx new file mode 100644 index 0000000000..e953d0a910 --- /dev/null +++ b/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 }> = ({ + unsavedDocs, +}) => ( +
+ {[...unsavedDocs.entries()].map( + ([docId, seconds]) => + seconds > 8 && ( + + ) + )} +
+) + +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 ( + + {t('saving_notification_with_seconds', { + docname: doc.entity.name, + seconds, + })} + + ) +} diff --git a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx new file mode 100644 index 0000000000..c136ec8d30 --- /dev/null +++ b/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 ( + {}} // It's not possible to hide this modal, but it's a required prop + className="lock-editor-modal" + backdrop={false} + keyboard={false} + > + + {t('connection_lost')} + + {t('sorry_the_connection_to_the_server_is_down')} + + ) +} diff --git a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx new file mode 100644 index 0000000000..f0ce93cf11 --- /dev/null +++ b/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()) + + // 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(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 && } + {unsavedDocs.size > 0 && } + + ) +} diff --git a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx index c2b96473e4..ded2cfcdfc 100644 --- a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx +++ b/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, ] diff --git a/services/web/frontend/js/features/ide-react/context/modals-context.tsx b/services/web/frontend/js/features/ide-react/context/modals-context.tsx index 1e96be9c5c..b1291c4ed9 100644 --- a/services/web/frontend/js/features/ide-react/context/modals-context.tsx +++ b/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(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( () => ({ 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} /> - ) } diff --git a/services/web/frontend/js/features/ide-react/editor/open-documents.ts b/services/web/frontend/js/features/ide-react/editor/open-documents.ts index eef90b5d82..3efbbe514b 100644 --- a/services/web/frontend/js/features/ide-react/editor/open-documents.ts +++ b/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 } } diff --git a/services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts b/services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts deleted file mode 100644 index 386a919bdd..0000000000 --- a/services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts +++ /dev/null @@ -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, - ]) -} diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index ac2561c0e1..0f70ed4b22 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/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' diff --git a/services/web/frontend/js/features/project-list/components/notifications/system-message.tsx b/services/web/frontend/js/shared/components/system-message.tsx similarity index 77% rename from services/web/frontend/js/features/project-list/components/notifications/system-message.tsx rename to services/web/frontend/js/shared/components/system-message.tsx index 5829ea23f3..8a59c42921 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/system-message.tsx +++ b/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 diff --git a/services/web/frontend/js/features/project-list/components/notifications/system-messages.tsx b/services/web/frontend/js/shared/components/system-messages.tsx similarity index 84% rename from services/web/frontend/js/features/project-list/components/notifications/system-messages.tsx rename to services/web/frontend/js/shared/components/system-messages.tsx index 6133349862..8302d15a60 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/system-messages.tsx +++ b/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 diff --git a/services/web/frontend/js/features/project-list/components/notifications/translation-message.tsx b/services/web/frontend/js/shared/components/translation-message.tsx similarity index 80% rename from services/web/frontend/js/features/project-list/components/notifications/translation-message.tsx rename to services/web/frontend/js/shared/components/translation-message.tsx index 6aab72c809..d669f807db 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/translation-message.tsx +++ b/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() diff --git a/services/web/frontend/js/shared/context/editor-context.jsx b/services/web/frontend/js/shared/context/editor-context.jsx index 57f4892ca0..58c131a007 100644 --- a/services/web/frontend/js/shared/context/editor-context.jsx +++ b/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, diff --git a/services/web/frontend/stories/project-list/system-messages.stories.tsx b/services/web/frontend/stories/project-list/system-messages.stories.tsx index 5280ef40b0..990f0a0856 100644 --- a/services/web/frontend/stories/project-list/system-messages.stories.tsx +++ b/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' diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 851e7bfa56..706cb30f4b 100644 --- a/services/web/locales/en.json +++ b/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 contact us 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", diff --git a/services/web/test/frontend/features/project-list/components/system-messages.test.tsx b/services/web/test/frontend/features/project-list/components/system-messages.test.tsx index 73cb8667b7..52c7945474 100644 --- a/services/web/test/frontend/features/project-list/components/system-messages.test.tsx +++ b/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('', function () { beforeEach(function () { diff --git a/services/web/types/project/dashboard/system-message.ts b/services/web/types/system-message.ts similarity index 100% rename from services/web/types/project/dashboard/system-message.ts rename to services/web/types/system-message.ts