// Disable prop type checks for test harnesses /* eslint-disable react/prop-types */ import { merge } from 'lodash' import { SocketIOMock } from '@/ide/connection/SocketIoShim' import { IdeContext } from '@/shared/context/ide-context' import React, { useCallback, useEffect, useState, useMemo, type FC, type PropsWithChildren, } from 'react' 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, type EditorOpenDocContextState, } from '@/features/ide-react/context/editor-open-doc-context' import { ProjectContext } from '@/shared/context/project-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 useExposedState from '@/shared/hooks/use-exposed-state' import { ProjectSnapshot } from '@/infrastructure/project-snapshot' import { EditorPropertiesContext, EditorPropertiesContextValue, } from '@/features/ide-react/context/editor-properties-context' import { type IdeLayout, type IdeView, LayoutContext, type LayoutContextValue, } from '@/shared/context/layout-context' import type { Socket } from '@/features/ide-react/connection/types/socket' import type { PermissionsLevel } from '@/features/ide-react/types/permissions' import type { Folder } from '../../../types/folder' import type { SocketDebuggingInfo } from '@/features/ide-react/connection/types/connection-state' import type { DocumentContainer } from '@/features/ide-react/editor/document-container' import { ProjectMetadata, ProjectUpdate, } from '@/shared/context/types/project-metadata' import { UserId } from '../../../types/user' import { ProjectCompiler } from '../../../types/project-settings' // these constants can be imported in tests instead of // using magic strings export const PROJECT_ID = 'project123' export const PROJECT_NAME = 'project-name' export const USER_ID = '123abd' export const USER_EMAIL = 'testuser@example.com' const defaultUserSettings = { pdfViewer: 'pdfjs', fontSize: 12, fontFamily: 'monaco', lineHeight: 'normal', editorTheme: 'textmate', overallTheme: '', mode: 'default', autoComplete: true, autoPairDelimiters: true, trackChanges: true, syntaxValidation: false, mathPreview: true, } export type EditorProvidersProps = { user?: { id: string; email: string } projectId?: string projectName?: string projectOwner?: ProjectMetadata['owner'] rootDocId?: string imageName?: string compiler?: ProjectCompiler socket?: Socket isRestrictedTokenMember?: boolean scope?: Record features?: Record projectFeatures?: Record permissionsLevel?: PermissionsLevel children?: React.ReactNode rootFolder?: Folder[] layoutContext?: Partial userSettings?: Record providers?: Record>> } export const projectDefaults = { _id: PROJECT_ID, name: PROJECT_NAME, owner: { _id: '124abd' as UserId, email: 'owner@example.com', first_name: 'Test', last_name: 'Owner', privileges: 'owner', signUpDate: new Date('2025-07-07').toISOString(), }, features: { referencesSearch: true, gitBridge: false, }, rootDocId: '_root_doc_id', rootFolder: [ { _id: 'root-folder-id', name: 'rootFolder', docs: [ { _id: '_root_doc_id', name: 'main.tex', }, ], folders: [], fileRefs: [], }, ], imageName: 'texlive-full:2024.1', compiler: 'pdflatex' as ProjectCompiler, members: [], invites: [], } /** * @typedef {import('@/shared/context/layout-context').LayoutContextValue} LayoutContextValue * @type Partial */ const layoutContextDefault = { view: 'editor', openFile: null, chatIsOpen: true, // false in the application, true in tests reviewPanelOpen: false, miniReviewPanelVisible: false, leftMenuShown: false, projectSearchIsOpen: false, pdfLayout: 'sideBySide', loadingStyleSheet: false, } satisfies Partial export function EditorProviders({ user = { id: USER_ID, email: USER_EMAIL }, projectId = projectDefaults._id, projectName = projectDefaults.name, projectOwner = projectDefaults.owner, rootDocId = projectDefaults.rootDocId, imageName = projectDefaults.imageName, compiler = projectDefaults.compiler, socket = new SocketIOMock() as any as Socket, isRestrictedTokenMember = false, scope: defaultScope = {}, features = { referencesSearch: true, gitBridge: false, }, projectFeatures = features, permissionsLevel = 'owner', children, rootFolder = projectDefaults.rootFolder, /** @type {Partial} */ layoutContext = layoutContextDefault, userSettings = {}, providers = {}, }: EditorProvidersProps) { window.metaAttributesCache.set( 'ol-gitBridgePublicBaseUrl', 'https://git.overleaf.test' ) window.metaAttributesCache.set( 'ol-isRestrictedTokenMember', isRestrictedTokenMember ) window.metaAttributesCache.set( 'ol-userSettings', merge({}, defaultUserSettings, userSettings) ) window.metaAttributesCache.set('ol-capabilities', ['chat', 'dropbox']) const scope = merge( { user, editor: { sharejs_doc: { doc_id: 'test-doc', getSnapshot: () => 'some doc content', hasBufferedOps: () => false, on: () => {}, off: () => {}, leaveAndCleanUpPromise: async () => {}, } as any as DocumentContainer, openDocName: null, currentDocumentId: null, wantTrackChanges: false, }, permissionsLevel, }, defaultScope ) const project = { _id: projectId, name: projectName, owner: projectOwner, features: projectFeatures, rootDocId, rootFolder, imageName, compiler, members: [], invites: [], trackChangesState: false, spellCheckLanguage: 'en', } // Add details for useUserContext window.metaAttributesCache.set('ol-user', { ...user, features }) window.metaAttributesCache.set('ol-project_id', projectId) return ( {children} ) } const makeConnectionProvider = (socket: Socket) => { const ConnectionProvider: FC = ({ children }) => { const [value] = useState(() => ({ socket, connectionState: { readyState: WebSocket.OPEN, forceDisconnected: false, inactiveDisconnect: false, reconnectAt: null, forcedDisconnectDelay: 0, lastConnectionAttempt: 0, error: '' as const, }, isConnected: true, isStillReconnecting: false, secondsUntilReconnect: () => 0, tryReconnectNow: () => {}, registerUserActivity: () => {}, disconnect: () => {}, closeConnection: () => {}, getSocketDebuggingInfo: () => ({}) as SocketDebuggingInfo, })) return ( {children} ) } return ConnectionProvider } const makeIdeReactProvider = ( scope: Record, socket: Socket ) => { const IdeReactProvider: FC = ({ children }) => { const [startedFreeTrial, setStartedFreeTrial] = useState(false) const [ideReactContextValue] = useState(() => ({ projectId: PROJECT_ID, eventEmitter: new IdeEventEmitter(), startedFreeTrial, setStartedFreeTrial, reportError: () => {}, projectJoined: true, permissionsLevel: scope.permissionsLevel as PermissionsLevel, setPermissionsLevel: () => {}, setOutOfSync: () => {}, })) const [ideContextValue] = useState(() => { const scopeEventEmitter = new ReactScopeEventEmitter( new IdeEventEmitter() ) const unstableStore = new ReactScopeValueStore() return { socket, scopeEventEmitter, unstableStore, } }) useEffect(() => { window.overleaf = { ...window.overleaf, unstable: { ...window.overleaf?.unstable, store: ideContextValue.unstableStore, }, } }, [ideContextValue.unstableStore]) return ( {children} ) } return IdeReactProvider } export function makeEditorOpenDocProvider( initialValues: EditorOpenDocContextState ) { const { currentDocumentId: initialCurrentDocumentId, openDocName: initialOpenDocName, currentDocument: initialCurrentDocument, } = initialValues const EditorOpenDocProvider: FC = ({ 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?: Partial ) => { const layout = { ...layoutContextDefault, ...layoutContextOverrides, } const LayoutProvider: FC = ({ children }) => { const [view, setView] = useState(layout.view) const [openFile, setOpenFile] = useState(layout.openFile) const [chatIsOpen, setChatIsOpen] = useState(layout.chatIsOpen) const [reviewPanelOpen, setReviewPanelOpen] = useState( layout.reviewPanelOpen ) const [miniReviewPanelVisible, setMiniReviewPanelVisible] = useState( layout.miniReviewPanelVisible ) const [leftMenuShown, setLeftMenuShown] = useState(layout.leftMenuShown) const [projectSearchIsOpen, setProjectSearchIsOpen] = useState( layout.projectSearchIsOpen ) const [pdfLayout, setPdfLayout] = useState(layout.pdfLayout) const [loadingStyleSheet, setLoadingStyleSheet] = useState( layout.loadingStyleSheet ) useEventListener( 'ui.toggle-review-panel', useCallback(() => { setReviewPanelOpen(open => !open) }, [setReviewPanelOpen]) ) const changeLayout = useCallback( (newLayout: IdeLayout, newView: IdeView = 'editor') => { setPdfLayout(newLayout) setView(newLayout === 'sideBySide' ? 'editor' : newView) }, [setPdfLayout, setView] ) const restoreView = useCallback(() => { setView('editor') }, []) const { reattach, detach, isLinked: detachIsLinked, role: detachRole, } = useDetachLayout() const pdfPreviewOpen = pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher' const value = useMemo( () => ({ reattach, detach, detachIsLinked, detachRole, changeLayout, chatIsOpen, leftMenuShown, openFile, pdfLayout, pdfPreviewOpen, projectSearchIsOpen, setProjectSearchIsOpen, reviewPanelOpen, miniReviewPanelVisible, loadingStyleSheet, setChatIsOpen, setLeftMenuShown, setOpenFile, setPdfLayout, setReviewPanelOpen, setMiniReviewPanelVisible, setLoadingStyleSheet, setView, view, restoreView, }), [ reattach, detach, detachIsLinked, detachRole, changeLayout, chatIsOpen, leftMenuShown, openFile, pdfLayout, pdfPreviewOpen, projectSearchIsOpen, setProjectSearchIsOpen, reviewPanelOpen, miniReviewPanelVisible, loadingStyleSheet, setChatIsOpen, setLeftMenuShown, setOpenFile, setPdfLayout, setReviewPanelOpen, setMiniReviewPanelVisible, setLoadingStyleSheet, setView, view, restoreView, ] ) return ( {children} ) } return LayoutProvider } export function makeEditorPropertiesProvider( initialValues: Partial< Pick< EditorPropertiesContextValue, 'showVisual' | 'showSymbolPalette' | 'wantTrackChanges' > > ) { const EditorPropertiesProvider: FC = ({ children }) => { const { showVisual: initialShowVisual, showSymbolPalette: initialShowSymbolPalette, wantTrackChanges: initialWantTrackChanges, } = initialValues const [showVisual, setShowVisual] = useState(initialShowVisual || false) const [showSymbolPalette, setShowSymbolPalette] = useState( initialShowSymbolPalette || false ) function toggleSymbolPalette() { setShowSymbolPalette(show => !show) } const [opening, setOpening] = useState(true) const [trackChanges, setTrackChanges] = useState(false) const [wantTrackChanges, setWantTrackChanges] = useState( initialWantTrackChanges || false ) const [errorState, setErrorState] = useState(false) const value = { showVisual, setShowVisual, showSymbolPalette, setShowSymbolPalette, toggleSymbolPalette, opening, setOpening, trackChanges, setTrackChanges, wantTrackChanges, setWantTrackChanges, errorState, setErrorState, } return ( {children} ) } return EditorPropertiesProvider } export function makeProjectProvider(initialProject: ProjectMetadata) { const ProjectProvider: FC = ({ children }) => { const [project, setProject] = useState(initialProject) const updateProject = useCallback((projectUpdateData: ProjectUpdate) => { setProject(projectData => Object.assign({}, projectData, projectUpdateData) ) }, []) const value = { projectId: project._id, project, joinProject: () => {}, updateProject, joinedOnce: true, projectSnapshot: new ProjectSnapshot(project._id), tags: [], features: project.features, name: project.name, } return ( {children} ) } return ProjectProvider }