[web] Remove instances of $scope and _ide (#26297)

* Remove `$scope` from `getMockIde`

* Replace `...getMockIde()` by `_id: projectId`

* Simplify stories decorator scope.tsx: less reliance on `window`

* Update helper editor-providers.jsx: pass data instead directly instead of using `window`

* Remove `cleanUpContext`

* Remove unused prop `clsiServerId`

* Update types to reflect unused properties

* Remove comment

* Add `ol-chatEnabled` in Storybook

* Revert moving `getMeta` outside of the component

This causes issues in Storybook

GitOrigin-RevId: dc2558ce814c2d738fb39450c57c104f4419efb8
This commit is contained in:
Antoine Clausse
2025-06-18 09:18:46 +02:00
committed by Copybot
parent a559cbb590
commit 3ba002460e
13 changed files with 154 additions and 280 deletions

View File

@@ -13,6 +13,7 @@ import en from '../../../services/web/locales/en.json'
function resetMeta() {
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-i18n', { currentLangCode: 'en' })
window.metaAttributesCache.set('ol-chatEnabled', true)
window.metaAttributesCache.set('ol-ExposedSettings', {
adminEmail: 'placeholder@example.com',
appName: 'Overleaf',

View File

@@ -176,7 +176,6 @@ export default function FileTreeUploadDoc() {
// close the modal when all the uploads completed successfully
.on('complete', result => {
if (!result.failed.length) {
// $scope.$emit('done', { name: name })
cancel()
}
})

View File

@@ -16,7 +16,6 @@ import {
} from '@/features/ide-react/create-ide-event-emitter'
import { JoinProjectPayload } from '@/features/ide-react/connection/join-project-payload'
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import { getMockIde } from '@/shared/context/mock/mock-ide'
import { populateEditorScope } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
import { postJSON } from '@/infrastructure/fetch-json'
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
@@ -157,11 +156,11 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
const ide = useMemo(() => {
return {
...getMockIde(),
_id: projectId,
socket,
reportError,
}
}, [socket, reportError])
}, [projectId, socket, reportError])
const value = useMemo(
() => ({

View File

@@ -4,7 +4,6 @@ import { ScopeEventEmitter } from '../../../../types/ide/scope-event-emitter'
import { Socket } from '@/features/ide-react/connection/types/socket'
export type Ide = {
$scope: Record<string, any>
socket: Socket
}

View File

@@ -1,56 +0,0 @@
import getMeta from '../../../utils/meta'
// When rendered without Angular, ide isn't defined. In that case we use
// a mock object that only has the required properties to pass proptypes
// checks and the values needed for the app. In the longer term, the mock
// object will replace ide completely.
export const getMockIde = () => {
return {
_id: getMeta('ol-project_id'),
$scope: {
$on: () => {},
$watch: () => {},
$applyAsync: () => {},
user: {},
project: {
_id: getMeta('ol-project_id'),
name: getMeta('ol-projectName'),
rootDocId: '',
members: [],
invites: [],
features: {
collaborators: 0,
compileGroup: 'standard',
trackChangesVisible: false,
references: false,
mendeley: false,
zotero: false,
},
publicAccessLevel: '',
owner: {
_id: '',
email: '',
},
},
permissionsLevel: 'readOnly',
editor: {
sharejs_doc: null,
showSymbolPalette: false,
toggleSymbolPalette: () => {},
},
ui: {
view: 'pdf',
chatOpen: false,
reviewPanelOpen: false,
leftMenuShown: false,
pdfLayout: 'flat',
},
pdf: {
uncompiled: true,
logEntryAnnotations: {},
},
settings: { syntaxValidation: false, pdfViewer: 'pdfjs' },
hasLintingError: false,
},
}
}

View File

@@ -1,5 +1,4 @@
import React, { FC, useEffect, useState } from 'react'
import { get } from 'lodash'
import { User, UserId } from '../../../types/user'
import { Project } from '../../../types/project'
import {
@@ -24,86 +23,68 @@ import { ReactContextRoot } from '@/features/ide-react/context/react-context-roo
const scopeWatchers: [string, (value: any) => void][] = []
const initialize = () => {
const user: User = {
id: 'story-user' as UserId,
email: 'story-user@example.com',
allowedFreeTrial: true,
features: { dropbox: true, symbolPalette: true },
}
const user: User = {
id: 'story-user' as UserId,
email: 'story-user@example.com',
allowedFreeTrial: true,
features: { dropbox: true, symbolPalette: true },
}
const project: Project = {
_id: '63e21c07946dd8c76505f85a',
name: 'A Project',
features: { mendeley: true, zotero: true, referencesSearch: true },
tokens: {},
owner: {
_id: 'a-user',
email: 'stories@overleaf.com',
const project: Project = {
_id: '63e21c07946dd8c76505f85a',
name: 'A Project',
features: { mendeley: true, zotero: true, referencesSearch: true },
tokens: {},
owner: {
_id: 'a-user',
email: 'stories@overleaf.com',
},
members: [],
invites: [],
rootDoc_id: '5e74f1a7ce17ae0041dfd056',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: 'test-file-id', name: 'testfile.tex' },
{ _id: 'test-bib-file-id', name: 'testsources.bib' },
],
fileRefs: [{ _id: 'test-image-id', name: 'frog.jpg', hash: '42' }],
folders: [],
},
members: [],
invites: [],
rootDoc_id: '5e74f1a7ce17ae0041dfd056',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: 'test-file-id', name: 'testfile.tex' },
{ _id: 'test-bib-file-id', name: 'testsources.bib' },
],
fileRefs: [{ _id: 'test-image-id', name: 'frog.jpg', hash: '42' }],
folders: [],
},
],
}
],
}
const scope = {
user,
project,
$watch: (key: string, callback: () => void) => {
scopeWatchers.push([key, callback])
const initialScope = {
user,
project,
ui: {
chatOpen: true,
pdfLayout: 'flat',
},
settings: {
pdfViewer: 'js',
syntaxValidation: true,
},
editor: {
richText: false,
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
hasBufferedOps: () => false,
},
$applyAsync: (callback: () => void) => {
window.setTimeout(() => {
callback()
for (const [key, watcher] of scopeWatchers) {
watcher(get(ide.$scope, key))
}
}, 0)
},
$on: () => {
//
},
$broadcast: () => {},
ui: {
chatOpen: true,
pdfLayout: 'flat',
},
settings: {
pdfViewer: 'js',
syntaxValidation: true,
},
editor: {
richText: false,
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
hasBufferedOps: () => false,
},
open_doc_name: 'testfile.tex',
},
hasLintingError: false,
permissionsLevel: 'owner',
}
open_doc_name: 'testfile.tex',
},
hasLintingError: false,
permissionsLevel: 'owner',
}
const ide = {
$scope: scope,
socket: new SocketIOShim.SocketShimNoop(
new SocketIOMock()
) as unknown as Socket,
}
const socket = new SocketIOShim.SocketShimNoop(
new SocketIOMock()
) as unknown as Socket
const initializeMetaTags = () => {
// window.metaAttributesCache is reset in preview.tsx
window.metaAttributesCache.set('ol-user', user)
window.metaAttributesCache.set('ol-project_id', project._id)
@@ -111,8 +92,6 @@ const initialize = () => {
'ol-gitBridgePublicBaseUrl',
'https://git.stories.com'
)
window._ide = ide
}
type ScopeDecoratorOptions = {
@@ -125,7 +104,7 @@ export const ScopeDecorator = (
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true },
meta: Record<string, any> = {}
) => {
initialize()
initializeMetaTags()
// mock compile on load
useFetchMock(fetchMock => {
@@ -171,7 +150,7 @@ const ConnectionProvider: FC<React.PropsWithChildren> = ({ children }) => {
error: '',
}
return {
socket: window._ide.socket as Socket,
socket,
connectionState,
isConnected: true,
isStillReconnecting: false,
@@ -215,9 +194,8 @@ const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
}))
const [ideContextValue] = useState(() => {
const ide = window._ide
const scopeStore = createReactScopeValueStore(projectId)
for (const [key, value] of Object.entries(ide.$scope)) {
for (const [key, value] of Object.entries(initialScope)) {
scopeStore.set(key, value)
}
const scopeEventEmitter = new ReactScopeEventEmitter(new IdeEventEmitter())
@@ -231,7 +209,7 @@ const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
}
return {
...ide,
socket,
scopeStore,
scopeEventEmitter,
}

View File

@@ -7,10 +7,7 @@ import {
import fetchMock from 'fetch-mock'
import ChatPane from '../../../../../frontend/js/features/chat/components/chat-pane'
import {
cleanUpContext,
renderWithEditorContext,
} from '../../../helpers/render-with-context'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { stubMathJax, tearDownMathJaxStubs } from './stubs'
describe('<ChatPane />', function () {
@@ -47,8 +44,6 @@ describe('<ChatPane />', function () {
beforeEach(function () {
fetchMock.removeRoutes().clearHistory()
cleanUpContext()
stubMathJax()
})

View File

@@ -9,7 +9,6 @@ import {
useChatContext,
chatClientIdGenerator,
} from '@/features/chat/context/chat-context'
import { cleanUpContext } from '../../../helpers/render-with-context'
import { stubMathJax, tearDownMathJaxStubs } from '../components/stubs'
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
import { EditorProviders } from '../../../helpers/editor-providers'
@@ -24,7 +23,6 @@ describe('ChatContext', function () {
beforeEach(function () {
fetchMock.removeRoutes().clearHistory()
cleanUpContext()
stubMathJax()

View File

@@ -5,10 +5,7 @@ import fetchMock from 'fetch-mock'
import userEvent from '@testing-library/user-event'
import ShareProjectModal from '../../../../../frontend/js/features/share-project-modal/components/share-project-modal'
import {
renderWithEditorContext,
cleanUpContext,
} from '../../../helpers/render-with-context'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import {
EditorProviders,
USER_EMAIL,
@@ -100,7 +97,6 @@ describe('<ShareProjectModal/>', function () {
afterEach(function () {
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
cleanUpContext()
})
it('renders the modal', async function () {

View File

@@ -12,7 +12,7 @@ describe('<WordCountModal />', function () {
})
cy.mount(
<EditorProviders projectId="project-1" clsiServerId="clsi-server-1">
<EditorProviders projectId="project-1">
<WordCountModal show handleHide={cy.stub()} />
</EditorProviders>
)
@@ -30,7 +30,7 @@ describe('<WordCountModal />', function () {
})
cy.mount(
<EditorProviders projectId="project-1" clsiServerId="clsi-server-1">
<EditorProviders projectId="project-1">
<WordCountModal show handleHide={cy.stub()} />
</EditorProviders>
)
@@ -48,7 +48,7 @@ describe('<WordCountModal />', function () {
})
cy.mount(
<EditorProviders projectId="project-1" clsiServerId="clsi-server-1">
<EditorProviders projectId="project-1">
<WordCountModal show handleHide={cy.stub()} />
</EditorProviders>
)
@@ -64,7 +64,7 @@ describe('<WordCountModal />', function () {
})
cy.mount(
<EditorProviders projectId="project-1" clsiServerId="clsi-server-1">
<EditorProviders projectId="project-1">
<WordCountModal show handleHide={cy.stub()} />
</EditorProviders>
)
@@ -87,7 +87,7 @@ describe('<WordCountModal />', function () {
})
cy.mount(
<EditorProviders projectId="project-1" clsiServerId="clsi-server-1">
<EditorProviders projectId="project-1">
<WordCountModal show handleHide={cy.stub()} />
</EditorProviders>
)

View File

@@ -1,7 +1,6 @@
// Disable prop type checks for test harnesses
/* eslint-disable react/prop-types */
import sinon from 'sinon'
import { get, merge } from 'lodash'
import { merge } from 'lodash'
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
import { IdeContext } from '@/shared/context/ide-context'
import React, { useEffect, useState } from 'react'
@@ -48,8 +47,7 @@ export function EditorProviders({
compiler = 'pdflatex',
socket = new SocketIOMock(),
isRestrictedTokenMember = false,
clsiServerId = '1234',
scope = {},
scope: defaultScope = {},
features = {
referencesSearch: true,
},
@@ -71,18 +69,6 @@ export function EditorProviders({
},
],
ui = { view: 'editor', pdfLayout: 'sideBySide', chatOpen: true },
fileTreeManager = {
findEntityById: () => null,
findEntityByPath: () => null,
getEntityPath: () => '',
getRootDocDirname: () => '',
getPreviewByPath: path => ({ url: path, extension: 'png' }),
},
editorManager = {
getCurrentDocumentId: () => 'foo',
getCurrentDocValue: () => {},
openDoc: sinon.stub(),
},
userSettings = {},
providers = {},
}) {
@@ -99,7 +85,7 @@ export function EditorProviders({
merge({}, defaultUserSettings, userSettings)
)
const $scope = merge(
const scope = merge(
{
user,
editor: {
@@ -123,25 +109,11 @@ export function EditorProviders({
compiler,
},
ui,
$watch: (path, callback) => {
callback(get($scope, path))
return () => null
},
$on: sinon.stub(),
$applyAsync: sinon.stub(),
permissionsLevel,
},
scope
defaultScope
)
window._ide = {
$scope,
socket,
clsiServerId,
editorManager,
fileTreeManager,
}
// Add details for useUserContext
window.metaAttributesCache.set('ol-user', { ...user, features })
window.metaAttributesCache.set('ol-project_id', projectId)
@@ -149,8 +121,8 @@ export function EditorProviders({
return (
<ReactContextRoot
providers={{
ConnectionProvider,
IdeReactProvider,
ConnectionProvider: makeConnectionProvider(socket),
IdeReactProvider: makeIdeReactProvider(scope, socket),
...providers,
}}
>
@@ -159,79 +131,85 @@ export function EditorProviders({
)
}
const ConnectionProvider = ({ children }) => {
const [value] = useState(() => ({
socket: window._ide.socket,
connectionState: {
readyState: WebSocket.OPEN,
forceDisconnected: false,
inactiveDisconnect: false,
reconnectAt: null,
forcedDisconnectDelay: 0,
lastConnectionAttempt: 0,
error: '',
},
isConnected: true,
isStillReconnecting: false,
secondsUntilReconnect: () => 0,
tryReconnectNow: () => {},
registerUserActivity: () => {},
disconnect: () => {},
}))
return (
<ConnectionContext.Provider value={value}>
{children}
</ConnectionContext.Provider>
)
}
const IdeReactProvider = ({ children }) => {
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
const [ideReactContextValue] = useState(() => ({
projectId: PROJECT_ID,
eventEmitter: new IdeEventEmitter(),
startedFreeTrial,
setStartedFreeTrial,
reportError: () => {},
projectJoined: true,
}))
const [ideContextValue] = useState(() => {
const ide = window._ide
const scopeStore = createReactScopeValueStore(PROJECT_ID)
for (const [key, value] of Object.entries(ide.$scope)) {
// TODO: path for nested entries
scopeStore.set(key, value)
}
scopeStore.set('editor.sharejs_doc', ide.$scope.editor.sharejs_doc)
scopeStore.set('ui.chatOpen', ide.$scope.ui.chatOpen)
const scopeEventEmitter = new ReactScopeEventEmitter(new IdeEventEmitter())
return {
...ide,
scopeStore,
scopeEventEmitter,
}
})
useEffect(() => {
window.overleaf = {
...window.overleaf,
unstable: {
...window.overleaf?.unstable,
store: ideContextValue.scopeStore,
const makeConnectionProvider = socket => {
const ConnectionProvider = ({ children }) => {
const [value] = useState(() => ({
socket,
connectionState: {
readyState: WebSocket.OPEN,
forceDisconnected: false,
inactiveDisconnect: false,
reconnectAt: null,
forcedDisconnectDelay: 0,
lastConnectionAttempt: 0,
error: '',
},
}
}, [ideContextValue.scopeStore])
isConnected: true,
isStillReconnecting: false,
secondsUntilReconnect: () => 0,
tryReconnectNow: () => {},
registerUserActivity: () => {},
disconnect: () => {},
}))
return (
<IdeReactContext.Provider value={ideReactContextValue}>
<IdeContext.Provider value={ideContextValue}>
return (
<ConnectionContext.Provider value={value}>
{children}
</IdeContext.Provider>
</IdeReactContext.Provider>
)
</ConnectionContext.Provider>
)
}
return ConnectionProvider
}
const makeIdeReactProvider = (scope, socket) => {
const IdeReactProvider = ({ children }) => {
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
const [ideReactContextValue] = useState(() => ({
projectId: PROJECT_ID,
eventEmitter: new IdeEventEmitter(),
startedFreeTrial,
setStartedFreeTrial,
reportError: () => {},
projectJoined: true,
}))
const [ideContextValue] = useState(() => {
const scopeStore = createReactScopeValueStore(PROJECT_ID)
for (const [key, value] of Object.entries(scope)) {
// TODO: path for nested entries
scopeStore.set(key, value)
}
scopeStore.set('editor.sharejs_doc', scope.editor.sharejs_doc)
scopeStore.set('ui.chatOpen', scope.ui.chatOpen)
const scopeEventEmitter = new ReactScopeEventEmitter(
new IdeEventEmitter()
)
return {
socket,
scopeStore,
scopeEventEmitter,
}
})
useEffect(() => {
window.overleaf = {
...window.overleaf,
unstable: {
...window.overleaf?.unstable,
store: ideContextValue.scopeStore,
},
}
}, [ideContextValue.scopeStore])
return (
<IdeReactContext.Provider value={ideReactContextValue}>
<IdeContext.Provider value={ideContextValue}>
{children}
</IdeContext.Provider>
</IdeReactContext.Provider>
)
}
return IdeReactProvider
}

View File

@@ -18,7 +18,3 @@ export function renderWithEditorContext(
...renderOptions,
})
}
export function cleanUpContext() {
delete window._ide
}

View File

@@ -1,20 +1,11 @@
import 'recurly__recurly-js'
import { ScopeValueStore } from './ide/scope-value-store'
import { MetaAttributesCache } from '@/utils/meta'
import { Socket } from '@/features/ide-react/connection/types/socket'
declare global {
// eslint-disable-next-line no-unused-vars
interface Window {
metaAttributesCache: MetaAttributesCache
_ide: Record<string, unknown> & {
$scope: Record<string, unknown> & {
pdf?: {
logEntryAnnotations: Record<string, unknown>
}
}
socket: Socket
}
MathJax: Record<string, any>
// For react-google-recaptcha
recaptchaOptions?: {