diff --git a/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx b/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx index 77ffcd6cd4..72dd27262d 100644 --- a/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx +++ b/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx @@ -3,6 +3,7 @@ import React, { FC, lazy, Suspense } from 'react' import useScopeValue from '@/shared/hooks/use-scope-value' import SourceEditor from '@/features/source-editor/components/source-editor' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { EditorScopeValue } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter' import classNames from 'classnames' import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane' @@ -17,7 +18,8 @@ const SymbolPalettePane = lazy( export const EditorPane: FC = () => { const [editor] = useScopeValue('editor') const { selectedEntityCount, openEntity } = useFileTreeOpenContext() - const { currentDocumentId, isLoading } = useEditorManagerContext() + const { isLoading } = useEditorManagerContext() + const { currentDocumentId } = useEditorOpenDocContext() if (!currentDocumentId) { return null 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 3a96bfc48d..dd06a6af86 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 @@ -37,6 +37,7 @@ import { Update } from '@/features/history/services/types/update' import { useDebugDiffTracker } from '../hooks/use-debug-diff-tracker' import { useEditorContext } from '@/shared/context/editor-context' import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' export interface GotoOffsetOptions { gotoOffset: number @@ -53,8 +54,6 @@ interface OpenDocOptions export type EditorManager = { getEditorType: () => EditorType | null showSymbolPalette: boolean - currentDocument: DocumentContainer | null - currentDocumentId: DocId | null getCurrentDocValue: () => string | null getCurrentDocumentId: () => DocId | null setIgnoringExternalUpdates: (value: boolean) => void @@ -63,8 +62,6 @@ export type EditorManager = { openDocs: OpenDocuments openFileWithId: (fileId: string) => void openInitialDoc: (docId: string) => void - openDocName: string | null - setOpenDocName: (openDocName: string) => void isLoading: boolean trackChanges: boolean jumpToLine: (options: GotoLineOptions) => void @@ -104,14 +101,13 @@ export const EditorManagerProvider: FC = ({ 'editor.showSymbolPalette' ) const [showVisual] = useScopeValue('editor.showVisual') - const [currentDocument, setCurrentDocument] = - useScopeValue('editor.sharejs_doc') - const [currentDocumentId, setCurrentDocumentId] = useScopeValue( - 'editor.open_doc_id' - ) - const [openDocName, setOpenDocName] = useScopeValue( - 'editor.open_doc_name' - ) + const { + currentDocumentId, + setCurrentDocumentId, + setOpenDocName, + currentDocument, + setCurrentDocument, + } = useEditorOpenDocContext() const [opening, setOpening] = useScopeValue('editor.opening') const [errorState, setIsInErrorState] = useScopeValue('editor.error_state') @@ -667,16 +663,12 @@ export const EditorManagerProvider: FC = ({ () => ({ getEditorType, showSymbolPalette, - currentDocument, - currentDocumentId, getCurrentDocValue, getCurrentDocumentId, setIgnoringExternalUpdates, openDocWithId, openDoc, openDocs, - openDocName, - setOpenDocName, trackChanges, isLoading, openFileWithId, @@ -689,8 +681,6 @@ export const EditorManagerProvider: FC = ({ [ getEditorType, showSymbolPalette, - currentDocument, - currentDocumentId, getCurrentDocValue, getCurrentDocumentId, setIgnoringExternalUpdates, @@ -699,8 +689,6 @@ export const EditorManagerProvider: FC = ({ openDocs, openFileWithId, openInitialDoc, - openDocName, - setOpenDocName, trackChanges, isLoading, jumpToLine, diff --git a/services/web/frontend/js/features/ide-react/context/editor-open-doc-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-open-doc-context.tsx new file mode 100644 index 0000000000..dd4a470578 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/editor-open-doc-context.tsx @@ -0,0 +1,63 @@ +import { + createContext, + Dispatch, + FC, + PropsWithChildren, + SetStateAction, + useContext, + useState, +} from 'react' +import { DocId } from '../../../../../types/project-settings' +import useExposedState from '@/shared/hooks/use-exposed-state' +import { DocumentContainer } from '@/features/ide-react/editor/document-container' + +type EditorOpenDocContextValue = { + currentDocumentId: DocId | null + setCurrentDocumentId: Dispatch> + openDocName: string | null + setOpenDocName: Dispatch> + currentDocument: DocumentContainer | null + setCurrentDocument: Dispatch> +} + +export const EditorOpenDocContext = createContext< + EditorOpenDocContextValue | undefined +>(undefined) + +export const EditorOpenDocProvider: FC = ({ children }) => { + const [currentDocumentId, setCurrentDocumentId] = + useExposedState(null, 'editor.open_doc_id') + const [openDocName, setOpenDocName] = useExposedState( + null, + 'editor.open_doc_name' + ) + const [currentDocument, setCurrentDocument] = + useState(null) + + const value = { + currentDocumentId, + setCurrentDocumentId, + openDocName, + setOpenDocName, + currentDocument, + setCurrentDocument, + } + + return ( + + {children} + + ) +} + +export const useEditorOpenDocContext = (): EditorOpenDocContextValue => { + const context = useContext(EditorOpenDocContext) + + if (!context) { + throw new Error( + 'useEditorOpenDocContext is only available inside EditorOpenDocContext.Provider' + ) + } + + return context +} diff --git a/services/web/frontend/js/features/ide-react/context/editor-view-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-view-context.tsx new file mode 100644 index 0000000000..e6bc055afc --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/editor-view-context.tsx @@ -0,0 +1,49 @@ +import { + createContext, + Dispatch, + FC, + PropsWithChildren, + SetStateAction, + useContext, +} from 'react' +import { EditorView } from '@codemirror/view' +import useExposedState from '@/shared/hooks/use-exposed-state' + +export type EditorContextValue = { + view: EditorView | null + setView: Dispatch> +} + +// This provides access to the CodeMirror EditorView instance outside the editor +// component itself, including external extensions (in particular, Writefull) +export const EditorViewContext = createContext( + undefined +) + +export const EditorViewProvider: FC = ({ children }) => { + const [view, setView] = useExposedState( + null, + 'editor.view' + ) + + const value = { + view, + setView, + } + + return ( + + {children} + + ) +} + +export const useEditorViewContext = (): EditorContextValue => { + const context = useContext(EditorViewContext) + if (!context) { + throw new Error( + 'useEditorViewContext is only available inside EditorViewProvider' + ) + } + return context +} diff --git a/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx b/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx index b4afd446da..d24187abd0 100644 --- a/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx @@ -11,6 +11,7 @@ import { import { useProjectContext } from '@/shared/context/project-context' import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { FileTreeDocumentFindResult, FileTreeFileRefFindResult, @@ -40,8 +41,8 @@ export const FileTreeOpenProvider: FC = ({ }) => { const { rootDocId, owner } = useProjectContext() const { eventEmitter, projectJoined } = useIdeReactContext() - const { openDocWithId, currentDocumentId, openInitialDoc } = - useEditorManagerContext() + const { openDocWithId, openInitialDoc } = useEditorManagerContext() + const { currentDocumentId } = useEditorOpenDocContext() const { setOpenFile } = useLayoutContext() const [openEntity, setOpenEntity] = useState< FileTreeDocumentFindResult | FileTreeFileRefFindResult | null diff --git a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx index 63734d91aa..33413a52e7 100644 --- a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx @@ -81,6 +81,13 @@ export const IdeReactProvider: FC = ({ children }) => { const [scopeEventEmitter] = useState( () => new ReactScopeEventEmitter(eventEmitter) ) + const [unstableStore] = useState(() => { + const store = new ReactScopeValueStore() + // Add dummy editor.ready key for Writefull, that relies on this calling + // back once after watching it + store.set('editor.ready', undefined) + return store + }) const [startedFreeTrial, setStartedFreeTrial] = useState(false) const release = getMeta('ol-ExposedSettings')?.sentryRelease ?? null @@ -179,6 +186,7 @@ export const IdeReactProvider: FC = ({ children }) => { ide={ide} scopeStore={scopeStore} scopeEventEmitter={scopeEventEmitter} + unstableStore={unstableStore} > {children} diff --git a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx index 4d5ab28050..7fa6332a16 100644 --- a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx @@ -10,7 +10,7 @@ import { } from 'react' import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' import { useConnectionContext } from '@/features/ide-react/context/connection-context' -import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { getJSON, postJSON } from '@/infrastructure/fetch-json' import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context' import { useEditorContext } from '@/shared/context/editor-context' @@ -54,7 +54,7 @@ export const MetadataProvider: FC = ({ children }) => { const { onlineUsersCount } = useOnlineUsersContext() const { permissionsLevel } = useEditorContext() const permissions = usePermissionsContext() - const { currentDocument } = useEditorManagerContext() + const { currentDocument } = useEditorOpenDocContext() const { showGenericMessageModal } = useModalsContext() const [documents, setDocuments] = useState({}) diff --git a/services/web/frontend/js/features/ide-react/context/online-users-context.tsx b/services/web/frontend/js/features/ide-react/context/online-users-context.tsx index 1195f9ae7c..84a1b7d71f 100644 --- a/services/web/frontend/js/features/ide-react/context/online-users-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/online-users-context.tsx @@ -18,7 +18,7 @@ import useSocketListener from '@/features/ide-react/hooks/use-socket-listener' import { debugConsole } from '@/utils/debugging' import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter' import { getHueForUserId } from '@/shared/utils/colors' -import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' export type OnlineUser = { id: string @@ -70,7 +70,7 @@ export const OnlineUsersProvider: FC = ({ }) => { const { eventEmitter } = useIdeReactContext() const { socket } = useConnectionContext() - const { currentDocumentId } = useEditorManagerContext() + const { currentDocumentId } = useEditorOpenDocContext() const { fileTreeData } = useFileTreeData() const [onlineUsers, setOnlineUsers] = useState>({}) diff --git a/services/web/frontend/js/features/ide-react/context/outline-context.tsx b/services/web/frontend/js/features/ide-react/context/outline-context.tsx index 8e582e607d..ea3c38d105 100644 --- a/services/web/frontend/js/features/ide-react/context/outline-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/outline-context.tsx @@ -14,7 +14,7 @@ import * as eventTracking from '@/infrastructure/event-tracking' import { isValidTeXFile } from '@/main/is-valid-tex-file' import localStorage from '@/infrastructure/local-storage' import { useProjectContext } from '@/shared/context/project-context' -import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' export type PartialFlatOutline = { level: number @@ -118,7 +118,7 @@ export const OutlineProvider: FC = ({ children }) => { [flatOutline, currentlyHighlightedLine] ) - const { openDocName } = useEditorManagerContext() + const { openDocName } = useEditorOpenDocContext() const isTexFile = useMemo( () => (openDocName ? isValidTeXFile(openDocName) : false), [openDocName] diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx index 0eeb870a2b..fdac20dc0f 100644 --- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx +++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx @@ -4,7 +4,9 @@ import { ConnectionProvider } from './connection-context' import { DetachCompileProvider } from '@/shared/context/detach-compile-context' import { DetachProvider } from '@/shared/context/detach-context' import { EditorManagerProvider } from '@/features/ide-react/context/editor-manager-context' +import { EditorOpenDocProvider } from '@/features/ide-react/context/editor-open-doc-context' import { EditorProvider } from '@/shared/context/editor-context' +import { EditorViewProvider } from '@/features/ide-react/context/editor-view-context' import { FileTreeDataProvider } from '@/shared/context/file-tree-data-context' import { FileTreeOpenProvider } from '@/features/ide-react/context/file-tree-open-context' import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path' @@ -39,7 +41,9 @@ export const ReactContextRoot: FC< DetachCompileProvider, DetachProvider, EditorManagerProvider, + EditorOpenDocProvider, EditorProvider, + EditorViewProvider, FileTreeDataProvider, FileTreeOpenProvider, FileTreePathProvider, @@ -74,47 +78,51 @@ export const ReactContextRoot: FC< - - - - - - - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts b/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts index 42e03ff8f6..e4f0e5aa01 100644 --- a/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts +++ b/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts @@ -6,8 +6,6 @@ export type EditorScopeValue = { showSymbolPalette: false toggleSymbolPalette: () => void sharejs_doc: DocumentContainer | null - open_doc_id: string | null - open_doc_name: string | null opening: boolean trackChanges: boolean wantTrackChanges: boolean @@ -25,8 +23,6 @@ export function populateEditorScope( showSymbolPalette: false, toggleSymbolPalette: () => {}, sharejs_doc: null, - open_doc_id: null, - open_doc_name: null, opening: true, trackChanges: false, wantTrackChanges: false, diff --git a/services/web/frontend/js/features/ide-redesign/components/editor.tsx b/services/web/frontend/js/features/ide-redesign/components/editor.tsx index 6c5e0b40db..ae861f9b6f 100644 --- a/services/web/frontend/js/features/ide-redesign/components/editor.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/editor.tsx @@ -1,5 +1,5 @@ import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane' -import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { EditorScopeValue } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter' import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context' import useScopeValue from '@/shared/hooks/use-scope-value' @@ -14,16 +14,16 @@ import SymbolPalettePane from '@/features/ide-react/components/editor/symbol-pal export const Editor = () => { const [editor] = useScopeValue('editor') const { selectedEntityCount, openEntity } = useFileTreeOpenContext() - const { currentDocumentId } = useEditorManagerContext() + const { currentDocumentId, currentDocument } = useEditorOpenDocContext() if (!currentDocumentId) { return null } const isLoading = Boolean( - (!editor.sharejs_doc || editor.opening) && + (!currentDocument || editor.opening) && !editor.error_state && - editor.open_doc_id + currentDocumentId ) return ( diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx index 0b625bc79b..46c20b24d6 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx @@ -12,7 +12,7 @@ export const usePdfPreviewContext = () => { const context = useContext(PdfPreviewContext) if (!context) { throw new Error( - 'usePdfPreviewContext is only avalable inside PdfPreviewProvider' + 'usePdfPreviewContext is only available inside PdfPreviewProvider' ) } return context diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts b/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts index 77986bfeac..667374bb66 100644 --- a/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts +++ b/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts @@ -13,6 +13,7 @@ import * as eventTracking from '../../../infrastructure/event-tracking' import { debugConsole } from '@/utils/debugging' import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import useEventListener from '@/shared/hooks/use-event-listener' import { CursorPosition } from '@/features/ide-react/types/cursor-position' import { isValidTeXFile } from '@/main/is-valid-tex-file' @@ -34,8 +35,8 @@ export default function useSynctex(): { const { selectedEntities } = useFileTreeData() const { findEntityByPath, dirname, pathInFolder } = useFileTreePathContext() - const { getCurrentDocumentId, openDocWithId, openDocName } = - useEditorManagerContext() + const { openDocName } = useEditorOpenDocContext() + const { getCurrentDocumentId, openDocWithId } = useEditorManagerContext() const [cursorPosition, setCursorPosition] = useState( () => { diff --git a/services/web/frontend/js/features/review-panel-new/context/ranges-context.tsx b/services/web/frontend/js/features/review-panel-new/context/ranges-context.tsx index 7066a78bb3..f5e9c94c09 100644 --- a/services/web/frontend/js/features/review-panel-new/context/ranges-context.tsx +++ b/services/web/frontend/js/features/review-panel-new/context/ranges-context.tsx @@ -21,7 +21,7 @@ import { useIdeReactContext } from '@/features/ide-react/context/ide-react-conte import { useConnectionContext } from '@/features/ide-react/context/connection-context' import useSocketListener from '@/features/ide-react/hooks/use-socket-listener' import { throttle } from 'lodash' -import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' export type Ranges = { docId: string @@ -81,7 +81,7 @@ const RangesActionsContext = createContext(undefined) export const RangesProvider: FC = ({ children }) => { const view = useCodeMirrorViewContext() const { projectId } = useIdeReactContext() - const { currentDocument } = useEditorManagerContext() + const { currentDocument } = useEditorOpenDocContext() const { socket } = useConnectionContext() const [ranges, setRanges] = useState(() => buildRanges(currentDocument) diff --git a/services/web/frontend/js/features/review-panel-new/context/threads-context.tsx b/services/web/frontend/js/features/review-panel-new/context/threads-context.tsx index 0a5c737585..5ad69245c7 100644 --- a/services/web/frontend/js/features/review-panel-new/context/threads-context.tsx +++ b/services/web/frontend/js/features/review-panel-new/context/threads-context.tsx @@ -20,7 +20,7 @@ import { UserId } from '../../../../../types/user' import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json' import RangesTracker from '@overleaf/ranges-tracker' import { CommentOperation } from '../../../../../types/change' -import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { useEditorContext } from '@/shared/context/editor-context' import { debugConsole } from '@/utils/debugging' import { captureException } from '@/infrastructure/error-reporter' @@ -50,7 +50,7 @@ const ThreadsActionsContext = createContext( export const ThreadsProvider: FC = ({ children }) => { const { _id: projectId } = useProjectContext() - const { currentDocument } = useEditorManagerContext() + const { currentDocument } = useEditorOpenDocContext() const { isRestrictedTokenMember } = useEditorContext() // const [error, setError] = useState() diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-view.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-view.tsx index 06484db1b6..4968e10367 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-view.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-view.tsx @@ -1,12 +1,12 @@ import { memo, useCallback, useEffect } from 'react' import { useCodeMirrorViewContext } from './codemirror-context' import useCodeMirrorScope from '../hooks/use-codemirror-scope' -import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only' +import { useEditorViewContext } from '@/features/ide-react/context/editor-view-context' function CodeMirrorView() { const view = useCodeMirrorViewContext() - const [, setView] = useScopeValueSetterOnly('editor.view') + const { setView } = useEditorViewContext() // append the editor view dom to the container node when mounted const containerRef = useCallback( @@ -25,7 +25,8 @@ function CodeMirrorView() { } }, [view]) - // add the editor view to the scope value store, so it can be accessed by external extensions + // Add the CodeMirror view to the editor view context so that it can be + // accessed outside the editor component useEffect(() => { setView(view) }, [setView, view]) diff --git a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx index c042c1bc94..7e8c950db0 100644 --- a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx +++ b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx @@ -4,12 +4,12 @@ import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import { sendMB } from '../../../infrastructure/event-tracking' import { isValidTeXFile } from '../../../main/is-valid-tex-file' import { useTranslation } from 'react-i18next' -import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' function EditorSwitch() { const { t } = useTranslation() const [visual, setVisual] = useScopeValue('editor.showVisual') - const { openDocName } = useEditorManagerContext() + const { openDocName } = useEditorOpenDocContext() const richTextAvailable = openDocName ? isValidTeXFile(openDocName) : false diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index 2504afdd0c..6c20a44702 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -57,6 +57,7 @@ import { } from '@/features/ide-react/context/editor-manager-context' import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options' import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' function useCodeMirrorScope(view: EditorView) { const { fileTreeData } = useFileTreeData() @@ -68,8 +69,8 @@ function useCodeMirrorScope(view: EditorView) { const { logEntryAnnotations, editedSinceCompileStarted, compiling } = useCompileContext() - const { currentDocument, openDocName, trackChanges } = - useEditorManagerContext() + const { openDocName, currentDocument } = useEditorOpenDocContext() + const { trackChanges } = useEditorManagerContext() const metadata = useMetadataContext() const { id: userId } = useUserContext() diff --git a/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx b/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx index 899d8de2f5..4dd58958ac 100644 --- a/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx +++ b/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx @@ -6,6 +6,7 @@ import { useProjectContext } from '@/shared/context/project-context' import useAbortController from '@/shared/hooks/use-abort-controller' import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' import { debugConsole } from '@/utils/debugging' import { signalWithTimeout } from '@/utils/abort-signal' @@ -20,7 +21,8 @@ export const WordCountClient: FC = () => { const [data, setData] = useState(null) const { projectSnapshot, rootDocId } = useProjectContext() const { spellCheckLanguage } = useProjectSettingsContext() - const { openDocs, currentDocument } = useEditorManagerContext() + const { openDocs } = useEditorManagerContext() + const { currentDocument } = useEditorOpenDocContext() const { pathInFolder } = useFileTreePathContext() const { signal } = useAbortController() diff --git a/services/web/frontend/js/shared/context/file-tree-data-context.tsx b/services/web/frontend/js/shared/context/file-tree-data-context.tsx index 37de214301..7e426b9e54 100644 --- a/services/web/frontend/js/shared/context/file-tree-data-context.tsx +++ b/services/web/frontend/js/shared/context/file-tree-data-context.tsx @@ -18,7 +18,7 @@ import { import { countFiles } from '../../features/file-tree/util/count-in-tree' import useDeepCompareEffect from '../../shared/hooks/use-deep-compare-effect' import { docsInFolder } from '@/features/file-tree/util/docs-in-folder' -import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { Folder } from '../../../../types/folder' import { Project } from '../../../../types/project' import { MainDocument } from '../../../../types/project-settings' @@ -180,8 +180,7 @@ export const FileTreeDataProvider: FC = ({ children, }) => { const [project] = useScopeValue('project') - const [currentDocumentId] = useScopeValue('editor.open_doc_id') - const [, setOpenDocName] = useScopeValueSetterOnly('editor.open_doc_name') + const { currentDocumentId, setOpenDocName } = useEditorOpenDocContext() const [permissionsLevel] = useScopeValue('permissionsLevel') const { fileTreeFromHistory, snapshot, snapshotVersion } = useSnapshotContext() diff --git a/services/web/frontend/js/shared/context/ide-context.tsx b/services/web/frontend/js/shared/context/ide-context.tsx index 1b4d83d56c..899dcbce5b 100644 --- a/services/web/frontend/js/shared/context/ide-context.tsx +++ b/services/web/frontend/js/shared/context/ide-context.tsx @@ -10,6 +10,7 @@ export type Ide = { type IdeContextValue = Ide & { scopeStore: ScopeValueStore scopeEventEmitter: ScopeEventEmitter + unstableStore: ScopeValueStore } export const IdeContext = createContext(undefined) @@ -19,14 +20,14 @@ export const IdeProvider: FC< ide: Ide scopeStore: ScopeValueStore scopeEventEmitter: ScopeEventEmitter + unstableStore: ScopeValueStore }> -> = ({ ide, scopeStore, scopeEventEmitter, children }) => { +> = ({ ide, scopeStore, scopeEventEmitter, unstableStore, children }) => { /** - * Expose scopeStore via `window.overleaf.unstable.store`, so it can be accessed by external extensions. + * Expose unstableStore via `window.overleaf.unstable.store`, so it can be accessed by external extensions. * * These properties are expected to be available: * - `editor.view` - * - `project.spellcheckLanguage` * - `editor.open_doc_name`, * - `editor.open_doc_id`, * - `settings.theme` @@ -40,18 +41,19 @@ export const IdeProvider: FC< ...window.overleaf, unstable: { ...window.overleaf?.unstable, - store: scopeStore, + store: unstableStore, }, } - }, [scopeStore]) + }, [unstableStore]) const value = useMemo(() => { return { ...ide, scopeStore, scopeEventEmitter, + unstableStore, } - }, [ide, scopeStore, scopeEventEmitter]) + }, [ide, scopeStore, scopeEventEmitter, unstableStore]) return {children} } diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx index 52aa0eee6d..8c8a7b4885 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -39,6 +39,7 @@ import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree- import { useUserSettingsContext } from '@/shared/context/user-settings-context' import { useFeatureFlag } from '@/shared/context/split-test-context' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { getJSON } from '@/infrastructure/fetch-json' import { CompileResponseData } from '../../../../types/compile' import { @@ -127,7 +128,8 @@ export const LocalCompileProvider: FC = ({ children, }) => { const { hasPremiumCompile, isProjectOwner } = useEditorContext() - const { openDocWithId, openDocs, currentDocument } = useEditorManagerContext() + const { openDocWithId, openDocs } = useEditorManagerContext() + const { currentDocument } = useEditorOpenDocContext() const { role } = useDetachContext() const { diff --git a/services/web/frontend/js/shared/context/user-settings-context.tsx b/services/web/frontend/js/shared/context/user-settings-context.tsx index b368371013..cc18c896fa 100644 --- a/services/web/frontend/js/shared/context/user-settings-context.tsx +++ b/services/web/frontend/js/shared/context/user-settings-context.tsx @@ -9,11 +9,11 @@ import { useEffect, } from 'react' -import { UserSettings, Keybindings } from '../../../../types/user-settings' +import { UserSettings } from '../../../../types/user-settings' import getMeta from '@/utils/meta' -import useScopeValue from '@/shared/hooks/use-scope-value' import { userStyles } from '../utils/styles' import { canUseNewEditor } from '@/features/ide-redesign/utils/new-editor-utils' +import { useIdeContext } from '@/shared/context/ide-context' const defaultSettings: UserSettings = { pdfViewer: 'pdfjs', @@ -39,15 +39,6 @@ type UserSettingsContextValue = { > } -type ScopeSettings = { - overallTheme: 'light' | 'dark' - keybindings: Keybindings - fontSize: number - fontFamily: string - lineHeight: number - isNewEditor: boolean -} - export const UserSettingsContext = createContext< UserSettingsContextValue | undefined >(undefined) @@ -60,10 +51,10 @@ export const UserSettingsProvider: FC = ({ ) // update the global scope 'settings' value, for extensions - const [, setScopeSettings] = useScopeValue('settings') + const { unstableStore } = useIdeContext() useEffect(() => { const { fontFamily, lineHeight } = userStyles(userSettings) - setScopeSettings({ + unstableStore.set('settings', { overallTheme: userSettings.overallTheme === 'light-' ? 'light' : 'dark', keybindings: userSettings.mode === 'none' ? 'default' : userSettings.mode, fontFamily, @@ -71,7 +62,7 @@ export const UserSettingsProvider: FC = ({ fontSize: userSettings.fontSize, isNewEditor: canUseNewEditor() && userSettings.enableNewEditor, }) - }, [setScopeSettings, userSettings]) + }, [unstableStore, userSettings]) const value = useMemo( () => ({ diff --git a/services/web/frontend/js/shared/hooks/use-exposed-state.ts b/services/web/frontend/js/shared/hooks/use-exposed-state.ts new file mode 100644 index 0000000000..520ed4356c --- /dev/null +++ b/services/web/frontend/js/shared/hooks/use-exposed-state.ts @@ -0,0 +1,25 @@ +import { type Dispatch, type SetStateAction, useEffect, useState } from 'react' +import { useIdeContext } from '../context/ide-context' + +/** + * Creates a state variable that is exposed via window.overleaf.unstable.store, + * which is used by Writefull (and only Writefull). Once Writefull is integrated + * into our codebase, it should be able to hook directly into our React + * contexts and we would then be able to remove this hook, replacing it with + * useState. + */ +export default function useExposedState( + initialState: T | (() => T), + path: string +): [T, Dispatch>] { + const [value, setValue] = useState(initialState) + + const { unstableStore } = useIdeContext() + + // Update the unstable store whenever the value changes + useEffect(() => { + unstableStore.set(path, value) + }, [unstableStore, path, value]) + + return [value, setValue] +} diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 00ad7f7daa..c81b9b6103 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -15,6 +15,7 @@ import { createReactScopeValueStore, } from '@/features/ide-react/context/ide-react-context' import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter' +import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store' import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter' import { ConnectionContext } from '@/features/ide-react/context/connection-context' import { Socket } from '@/features/ide-react/connection/types/socket' @@ -65,6 +66,8 @@ const initialScope = { }, editor: { richText: false, + + // FIXME: This is pretty useless because the editor relies on a much more fleshed-out document, so we rely on overriding it in individual stories sharejs_doc: { doc_id: 'test-doc', getSnapshot: () => 'some doc content', @@ -195,12 +198,13 @@ const IdeReactProvider: FC = ({ children }) => { scopeStore.set(key, value) } const scopeEventEmitter = new ReactScopeEventEmitter(new IdeEventEmitter()) + const unstableStore = new ReactScopeValueStore() window.overleaf = { ...window.overleaf, unstable: { ...window.overleaf?.unstable, - store: scopeStore, + store: unstableStore, }, } @@ -208,6 +212,7 @@ const IdeReactProvider: FC = ({ children }) => { socket, scopeStore, scopeEventEmitter, + unstableStore, } }) diff --git a/services/web/frontend/stories/pdf-log-entry.stories.tsx b/services/web/frontend/stories/pdf-log-entry.stories.tsx index 6b60bdff03..12f7594621 100644 --- a/services/web/frontend/stories/pdf-log-entry.stories.tsx +++ b/services/web/frontend/stories/pdf-log-entry.stories.tsx @@ -4,7 +4,7 @@ import { ruleIds } from '@/ide/human-readable-logs/HumanReadableLogsHints' import { ScopeDecorator } from './decorators/scope' import { useMeta } from './hooks/use-meta' import { FC, ReactNode } from 'react' -import { useScope } from './hooks/use-scope' +import { EditorViewContext } from '@/features/ide-react/context/editor-view-context' import { EditorView } from '@codemirror/view' import { LogEntry } from '@/features/pdf-preview/util/types' @@ -58,12 +58,30 @@ export default meta type Story = StoryObj +const MockEditorViewProvider: FC = ({ children }) => { + const value = { + view: new EditorView({ + doc: '\\begin{document', + }), + setView: () => {}, + } + + return ( + + {children} + + ) +} + const Provider: FC> = ({ children, }) => { useMeta({ 'ol-showAiErrorAssistant': true }) - useScope({ 'editor.view': new EditorView({ doc: '\\begin{document' }) }) - return
{children}
+ return ( + +
{children}
+
+ ) } export const PdfLogEntryWithControls: Story = { diff --git a/services/web/frontend/stories/source-editor/source-editor.stories.tsx b/services/web/frontend/stories/source-editor/source-editor.stories.tsx index 184c4faa46..7194adcef7 100644 --- a/services/web/frontend/stories/source-editor/source-editor.stories.tsx +++ b/services/web/frontend/stories/source-editor/source-editor.stories.tsx @@ -2,9 +2,86 @@ import SourceEditor from '../../js/features/source-editor/components/source-edit import { ScopeDecorator } from '../decorators/scope' import { useScope } from '../hooks/use-scope' import { useMeta } from '../hooks/use-meta' -import { FC } from 'react' +import React, { FC, useState } from 'react' import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' import RangesTracker from '@overleaf/ranges-tracker' +import useExposedState from '@/shared/hooks/use-exposed-state' +import { EditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' +import { DocId } from '../../../types/project-settings' +import { StoryObj } from '@storybook/react' +import { DocumentContainer } from '@/features/ide-react/editor/document-container' + +type Story = StoryObj + +const EditorOpenDocProvider: FC< + React.PropsWithChildren<{ + initialOpenDocName: string | null + initialDocument: DocumentContainer + }> +> = ({ children, initialOpenDocName, initialDocument }) => { + const [currentDocumentId, setCurrentDocumentId] = + useExposedState(null, 'editor.open_doc_id') + const [openDocName, setOpenDocName] = useExposedState( + initialOpenDocName, + 'editor.open_doc_name' + ) + const [currentDocument, setCurrentDocument] = + useState(initialDocument) + + const value = { + currentDocumentId, + setCurrentDocumentId, + openDocName, + setOpenDocName, + currentDocument, + setCurrentDocument, + } + + return ( + + {children} + + ) +} + +const LatexEditorOpenDocProvider: FC = ({ + children, +}) => ( + + {children} + +) + +const MarkdownEditorOpenDocProvider: FC = ({ + children, +}) => ( + + {children} + +) + +const BibtexEditorOpenDocProvider: FC = ({ + children, +}) => ( + + {children} + +) const FileTreePathProvider: FC = ({ children }) => ( - ScopeDecorator(Story, { - mockCompileOnLoad: true, - providers: { FileTreePathProvider }, - }), (Story: any) => (
@@ -59,102 +131,144 @@ const permissions = { write: true, } -export const Latex = (args: any, { globals: { theme } }: any) => { - // FIXME: useScope has no effect - useScope({ - editor: { - sharejs_doc: mockDoc(content.tex, changes.tex), - open_doc_name: 'example.tex', - }, - rootFolder: { - name: 'rootFolder', - id: 'root-folder-id', - type: 'folder', - children: [ - { - name: 'example.tex.tex', - id: 'example-doc-id', - type: 'doc', +export const Latex: Story = { + decorators: [ + Story => + ScopeDecorator(Story, { + mockCompileOnLoad: true, + providers: { + FileTreePathProvider, + EditorOpenDocProvider: LatexEditorOpenDocProvider, + }, + }), + + (Story, { globals }) => { + // FIXME: useScope has no effect + useScope({ + rootFolder: { + name: 'rootFolder', + id: 'root-folder-id', + type: 'folder', + children: [ + { + name: 'example.tex.tex', + id: 'example-doc-id', + type: 'doc', + selected: false, + $$hashKey: 'object:89', + }, + { + name: 'frog.jpg', + id: 'frog-image-id', + type: 'file', + linkedFileData: null, + created: '2023-05-04T16:11:04.352Z', + $$hashKey: 'object:108', + }, + ], selected: false, - $$hashKey: 'object:89', }, - { - name: 'frog.jpg', - id: 'frog-image-id', - type: 'file', - linkedFileData: null, - created: '2023-05-04T16:11:04.352Z', - $$hashKey: 'object:108', + settings: { + ...settings, + overallTheme: globals.theme === 'default-' ? '' : globals.theme, }, - ], - selected: false, - }, - settings: { - ...settings, - overallTheme: theme === 'default-' ? '' : theme, - }, - permissions, - }) + permissions, + }) - useMeta({ - 'ol-showSymbolPalette': true, - }) + useMeta({ + 'ol-showSymbolPalette': true, + }) - return + return + }, + ], } -export const Markdown = (args: any, { globals: { theme } }: any) => { - useScope({ - editor: { - sharejs_doc: mockDoc(content.md, changes.md), - open_doc_name: 'example.md', - }, - settings: { - ...settings, - overallTheme: theme === 'default-' ? '' : theme, - }, - permissions, - }) +export const Markdown: Story = { + decorators: [ + Story => + ScopeDecorator(Story, { + mockCompileOnLoad: true, + providers: { + FileTreePathProvider, + EditorOpenDocProvider: MarkdownEditorOpenDocProvider, + }, + }), - return + (Story, { globals }) => { + // FIXME: useScope has no effect + useScope({ + settings: { + ...settings, + overallTheme: globals.theme === 'default-' ? '' : globals.theme, + }, + permissions, + }) + + return + }, + ], } -export const Visual = (args: any, { globals: { theme } }: any) => { - useScope({ - editor: { - sharejs_doc: mockDoc(content.tex, changes.tex), - open_doc_name: 'example.tex', - showVisual: true, - }, - settings: { - ...settings, - overallTheme: theme === 'default-' ? '' : theme, - }, - permissions, - }) - useMeta({ - 'ol-showSymbolPalette': true, - 'ol-mathJaxPath': 'https://unpkg.com/mathjax@3.2.2/es5/tex-svg-full.js', - 'ol-project_id': '63e21c07946dd8c76505f85a', - }) +export const Visual: Story = { + decorators: [ + Story => + ScopeDecorator(Story, { + mockCompileOnLoad: true, + providers: { + FileTreePathProvider, + EditorOpenDocProvider: LatexEditorOpenDocProvider, + }, + }), - return + (Story, { globals }) => { + // FIXME: useScope has no effect, so this does not default to the visual editor + useScope({ + editor: { + showVisual: true, + }, + settings: { + ...settings, + overallTheme: globals.theme === 'default-' ? '' : globals.theme, + }, + permissions, + }) + + useMeta({ + 'ol-showSymbolPalette': true, + 'ol-mathJaxPath': 'https://unpkg.com/mathjax@3.2.2/es5/tex-svg-full.js', + 'ol-project_id': '63e21c07946dd8c76505f85a', + }) + + return + }, + ], } -export const Bibtex = (args: any, { globals: { theme } }: any) => { - useScope({ - editor: { - sharejs_doc: mockDoc(content.bib, changes.bib), - open_doc_name: 'example.bib', - }, - settings: { - ...settings, - overallTheme: theme === 'default-' ? '' : theme, - }, - permissions, - }) +export const Bibtex: Story = { + decorators: [ + Story => + ScopeDecorator(Story, { + mockCompileOnLoad: true, + providers: { + FileTreePathProvider, + EditorOpenDocProvider: BibtexEditorOpenDocProvider, + }, + }), - return + (Story, { globals }) => { + // FIXME: useScope has no effect + useScope({ + settings: { + ...settings, + overallTheme: globals.theme === 'default-' ? '' : globals.theme, + }, + permissions, + }) + + return + }, + ], } const MAX_DOC_LENGTH = 2 * 1024 * 1024 // ol-maxDocLength diff --git a/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx index 60326d8d3f..a3067b566e 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx @@ -11,6 +11,7 @@ import { import { EditorView } from '@codemirror/view' import { OpenDocuments } from '@/features/ide-react/editor/open-documents' import { LogEntry } from '@/features/pdf-preview/util/types' +import { EditorViewContext } from '@/features/ide-react/context/editor-view-context' describe('', function () { const fakeFindEntityResult: FindResult = { @@ -48,6 +49,19 @@ describe('', function () { ) } + const EditorViewProvider: FC = ({ children }) => { + const value = { + view: new EditorView({ doc: '\\documentclass{article}' }), + setView: cy.stub(), + } + + return ( + + {children} + + ) + } + const logEntries: LogEntry[] = [ { file: 'main.tex', @@ -62,10 +76,6 @@ describe('', function () { }, ] - const scope = { - 'editor.view': new EditorView({ doc: '\\documentclass{article}' }), - } - beforeEach(function () { cy.interceptCompile() cy.interceptEvents() @@ -73,7 +83,7 @@ describe('', function () { it('displays human readable hint', function () { cy.mount( - + ) @@ -84,8 +94,11 @@ describe('', function () { it('opens doc on click', function () { cy.mount( @@ -114,8 +127,11 @@ describe('', function () { cy.mount( @@ -154,8 +170,11 @@ describe('', function () { cy.mount( diff --git a/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx index b2a64b89a4..f8c379c51e 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx @@ -3,7 +3,10 @@ import { cloneDeep } from 'lodash' import { useDetachCompileContext as useCompileContext } from '../../../../frontend/js/shared/context/detach-compile-context' import { useFileTreeData } from '../../../../frontend/js/shared/context/file-tree-data-context' import { useEffect } from 'react' -import { EditorProviders } from '../../helpers/editor-providers' +import { + EditorProviders, + makeEditorOpenDocProvider, +} from '../../helpers/editor-providers' import { mockScope } from './scope' import { detachChannel, testDetachChannel } from '../../helpers/detach-channel' import { FindResult } from '@/features/file-tree/util/path' @@ -73,6 +76,22 @@ const WithSelectedEntities = ({ return null } +function mockProviders() { + return { + EditorOpenDocProvider: makeEditorOpenDocProvider({ + openDocName: 'main.tex', + currentDocument: { + doc_id: 'test-doc', + getSnapshot: () => 'some doc content', + hasBufferedOps: () => false, + on: () => {}, + off: () => {}, + leaveAndCleanUpPromise: () => Promise.resolve(), + }, + }), + } +} + describe('', function () { beforeEach(function () { window.metaAttributesCache.set('ol-project_id', 'test-project') @@ -84,9 +103,10 @@ describe('', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + @@ -145,9 +165,10 @@ describe('', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + ', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + ', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + @@ -218,9 +241,10 @@ describe('', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + @@ -279,9 +303,10 @@ describe('', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + @@ -317,9 +342,10 @@ describe('', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + @@ -338,9 +364,10 @@ describe('', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + ) @@ -385,9 +412,10 @@ describe('', function () { cy.interceptCompile() const scope = mockScope() + const providers = mockProviders() cy.mount( - + diff --git a/services/web/test/frontend/components/pdf-preview/scope.tsx b/services/web/test/frontend/components/pdf-preview/scope.tsx index e174efbf73..397813faa2 100644 --- a/services/web/test/frontend/components/pdf-preview/scope.tsx +++ b/services/web/test/frontend/components/pdf-preview/scope.tsx @@ -6,7 +6,6 @@ export const mockScope = () => ({ pdfViewer: 'pdfjs', }, editor: { - open_doc_name: 'main.tex', sharejs_doc: { doc_id: 'test-doc', getSnapshot: () => 'some doc content', diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx index dfce8134d1..793ff7b16d 100644 --- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx +++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx @@ -12,6 +12,7 @@ import { USER_ID, } from '../../../helpers/editor-providers' import { location } from '@/shared/components/location' +import useScopeValue from '@/shared/hooks/use-scope-value' async function changePrivilegeLevel(screen, { current, next }) { const select = screen.getByDisplayValue(current) @@ -820,7 +821,14 @@ describe('', function () { fetchMock.get(`/project/${project._id}/tokens`, {}) fetchMock.post('express:/project/:projectId/settings/admin', 204) - renderWithEditorContext(, { + let setPublicAccessLevel = function () {} + + function WrappedModal() { + setPublicAccessLevel = useScopeValue('project.publicAccesLevel')[1] + return + } + + renderWithEditorContext(, { scope: { project: { ...project, publicAccesLevel: 'private' }, }, @@ -839,13 +847,10 @@ describe('', function () { publicAccessLevel: 'tokenBased', }) - // NOTE: updating the scoped project data manually, - // as the project data is usually updated via the websocket connection - window.overleaf.unstable.store.set('project', { - ...project, - publicAccesLevel: 'tokenBased', - }) - // watchCallbacks.project({ ...project, publicAccesLevel: 'tokenBased' }) + // NOTE: the project data is usually updated via the websocket connection + // but we can't do that so we're doing it via the scope value store (this + // will be via the project context when this value has been migrated) + setPublicAccessLevel('tokenBased') await screen.findByText('Link sharing is on') const disableButton = await screen.findByRole('button', { @@ -859,13 +864,7 @@ describe('', function () { publicAccessLevel: 'private', }) - // NOTE: updating the scoped project data manually, - // as the project data is usually updated via the websocket connection - window.overleaf.unstable.store.set('project', { - ...project, - publicAccesLevel: 'private', - }) - // watchCallbacks.project({ ...project, publicAccesLevel: 'private' }) + setPublicAccessLevel('private') await screen.findByText('Link sharing is off') }) diff --git a/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts b/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts index 1f78dcf93a..701e3bc4b3 100644 --- a/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts +++ b/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts @@ -17,8 +17,8 @@ export const mockScope = ( return { editor: { sharejs_doc: mockDoc(content, docOptions), - open_doc_name: 'test.tex', - open_doc_id: docId, + openDocName: 'test.tex', + currentDocumentId: docId, showVisual: false, wantTrackChanges: false, }, diff --git a/services/web/test/frontend/helpers/editor-providers.jsx b/services/web/test/frontend/helpers/editor-providers.jsx index 4f722525c7..f2ed404f66 100644 --- a/services/web/test/frontend/helpers/editor-providers.jsx +++ b/services/web/test/frontend/helpers/editor-providers.jsx @@ -9,12 +9,15 @@ import { IdeReactContext, } from '@/features/ide-react/context/ide-react-context' import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter' +import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store' import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter' import { ConnectionContext } from '@/features/ide-react/context/connection-context' +import { EditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { ReactContextRoot } from '@/features/ide-react/context/react-context-root' import useEventListener from '@/shared/hooks/use-event-listener' import useDetachLayout from '@/shared/hooks/use-detach-layout' import { LayoutContext } from '@/shared/context/layout-context' +import useExposedState from '@/shared/hooks/use-exposed-state' // these constants can be imported in tests instead of // using magic strings @@ -119,6 +122,8 @@ export function EditorProviders({ off: () => {}, leaveAndCleanUpPromise: async () => {}, }, + openDocName: null, + currentDocumentId: null, }, project: { _id: projectId, @@ -144,6 +149,11 @@ export function EditorProviders({ providers={{ ConnectionProvider: makeConnectionProvider(socket), IdeReactProvider: makeIdeReactProvider(scope, socket), + EditorOpenDocProvider: makeEditorOpenDocProvider({ + openDocId: scope.editor.currentDocumentId, + openDocName: scope.editor.openDocName, + currentDocument: scope.editor.sharejs_doc, + }), LayoutProvider: makeLayoutProvider(layoutContext), ...providers, }} @@ -202,15 +212,16 @@ const makeIdeReactProvider = (scope, socket) => { // TODO: path for nested entries scopeStore.set(key, value) } - scopeStore.set('editor.sharejs_doc', scope.editor.sharejs_doc) const scopeEventEmitter = new ReactScopeEventEmitter( new IdeEventEmitter() ) + const unstableStore = new ReactScopeValueStore() return { socket, scopeStore, scopeEventEmitter, + unstableStore, } }) @@ -219,10 +230,10 @@ const makeIdeReactProvider = (scope, socket) => { ...window.overleaf, unstable: { ...window.overleaf?.unstable, - store: ideContextValue.scopeStore, + store: ideContextValue.unstableStore, }, } - }, [ideContextValue.scopeStore]) + }, [ideContextValue.unstableStore]) return ( @@ -235,6 +246,44 @@ const makeIdeReactProvider = (scope, socket) => { return IdeReactProvider } +export function makeEditorOpenDocProvider(initialValues) { + const { + currentDocumentId: initialCurrentDocumentId, + openDocName: initialOpenDocName, + currentDocument: initialCurrentDocument, + } = initialValues + const EditorOpenDocProvider = ({ children }) => { + const [currentDocumentId, setCurrentDocumentId] = useExposedState( + initialCurrentDocumentId, + 'editor.open_doc_id' + ) + const [openDocName, setOpenDocName] = useExposedState( + initialOpenDocName, + 'editor.open_doc_name' + ) + const [currentDocument, setCurrentDocument] = useState( + initialCurrentDocument + ) + + const value = { + currentDocumentId, + setCurrentDocumentId, + openDocName, + setOpenDocName, + currentDocument, + setCurrentDocument, + } + + return ( + + {children} + + ) + } + + return EditorOpenDocProvider +} + const makeLayoutProvider = layoutContextOverrides => { const layout = { ...layoutContextDefault,