editor-providers.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. // Disable prop type checks for test harnesses
  2. /* eslint-disable react/prop-types */
  3. import { merge } from 'lodash'
  4. import { SocketIOMock } from '@/ide/connection/SocketIoShim'
  5. import { IdeContext } from '@/shared/context/ide-context'
  6. import React, { useCallback, useEffect, useMemo, useState } from 'react'
  7. import {
  8. createReactScopeValueStore,
  9. IdeReactContext,
  10. } from '@/features/ide-react/context/ide-react-context'
  11. import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
  12. import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
  13. import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
  14. import { ConnectionContext } from '@/features/ide-react/context/connection-context'
  15. import { EditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
  16. import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
  17. import useEventListener from '@/shared/hooks/use-event-listener'
  18. import useDetachLayout from '@/shared/hooks/use-detach-layout'
  19. import { LayoutContext } from '@/shared/context/layout-context'
  20. import useExposedState from '@/shared/hooks/use-exposed-state'
  21. // these constants can be imported in tests instead of
  22. // using magic strings
  23. export const PROJECT_ID = 'project123'
  24. export const PROJECT_NAME = 'project-name'
  25. export const USER_ID = '123abd'
  26. export const USER_EMAIL = 'testuser@example.com'
  27. const defaultUserSettings = {
  28. pdfViewer: 'pdfjs',
  29. fontSize: 12,
  30. fontFamily: 'monaco',
  31. lineHeight: 'normal',
  32. editorTheme: 'textmate',
  33. overallTheme: '',
  34. mode: 'default',
  35. autoComplete: true,
  36. autoPairDelimiters: true,
  37. trackChanges: true,
  38. syntaxValidation: false,
  39. mathPreview: true,
  40. }
  41. /**
  42. * @typedef {import('@/shared/context/layout-context').LayoutContextValue} LayoutContextValue
  43. * @type Partial<LayoutContextValue>
  44. */
  45. const layoutContextDefault = {
  46. view: 'editor',
  47. openFile: null,
  48. chatIsOpen: true, // false in the application, true in tests
  49. reviewPanelOpen: false,
  50. miniReviewPanelVisible: false,
  51. leftMenuShown: false,
  52. projectSearchIsOpen: false,
  53. pdfLayout: 'sideBySide',
  54. loadingStyleSheet: false,
  55. }
  56. export function EditorProviders({
  57. user = { id: USER_ID, email: USER_EMAIL },
  58. projectId = PROJECT_ID,
  59. projectOwner = {
  60. _id: '124abd',
  61. email: 'owner@example.com',
  62. },
  63. rootDocId = '_root_doc_id',
  64. imageName = 'texlive-full:2024.1',
  65. compiler = 'pdflatex',
  66. socket = new SocketIOMock(),
  67. isRestrictedTokenMember = false,
  68. scope: defaultScope = {},
  69. features = {
  70. referencesSearch: true,
  71. },
  72. projectFeatures = features,
  73. permissionsLevel = 'owner',
  74. children,
  75. rootFolder = [
  76. {
  77. _id: 'root-folder-id',
  78. name: 'rootFolder',
  79. docs: [
  80. {
  81. _id: '_root_doc_id',
  82. name: 'main.tex',
  83. },
  84. ],
  85. folders: [],
  86. fileRefs: [],
  87. },
  88. ],
  89. /** @type {Partial<LayoutContext>} */
  90. layoutContext = layoutContextDefault,
  91. userSettings = {},
  92. providers = {},
  93. }) {
  94. window.metaAttributesCache.set(
  95. 'ol-gitBridgePublicBaseUrl',
  96. 'https://git.overleaf.test'
  97. )
  98. window.metaAttributesCache.set(
  99. 'ol-isRestrictedTokenMember',
  100. isRestrictedTokenMember
  101. )
  102. window.metaAttributesCache.set(
  103. 'ol-userSettings',
  104. merge({}, defaultUserSettings, userSettings)
  105. )
  106. window.metaAttributesCache.set('ol-capabilities', ['chat', 'dropbox'])
  107. const scope = merge(
  108. {
  109. user,
  110. editor: {
  111. sharejs_doc: {
  112. doc_id: 'test-doc',
  113. getSnapshot: () => 'some doc content',
  114. hasBufferedOps: () => false,
  115. on: () => {},
  116. off: () => {},
  117. leaveAndCleanUpPromise: async () => {},
  118. },
  119. openDocName: null,
  120. currentDocumentId: null,
  121. },
  122. project: {
  123. _id: projectId,
  124. name: PROJECT_NAME,
  125. owner: projectOwner,
  126. features: projectFeatures,
  127. rootDocId,
  128. rootFolder,
  129. imageName,
  130. compiler,
  131. },
  132. permissionsLevel,
  133. },
  134. defaultScope
  135. )
  136. // Add details for useUserContext
  137. window.metaAttributesCache.set('ol-user', { ...user, features })
  138. window.metaAttributesCache.set('ol-project_id', projectId)
  139. return (
  140. <ReactContextRoot
  141. providers={{
  142. ConnectionProvider: makeConnectionProvider(socket),
  143. IdeReactProvider: makeIdeReactProvider(scope, socket),
  144. EditorOpenDocProvider: makeEditorOpenDocProvider({
  145. openDocId: scope.editor.currentDocumentId,
  146. openDocName: scope.editor.openDocName,
  147. currentDocument: scope.editor.sharejs_doc,
  148. }),
  149. LayoutProvider: makeLayoutProvider(layoutContext),
  150. ...providers,
  151. }}
  152. >
  153. {children}
  154. </ReactContextRoot>
  155. )
  156. }
  157. const makeConnectionProvider = socket => {
  158. const ConnectionProvider = ({ children }) => {
  159. const [value] = useState(() => ({
  160. socket,
  161. connectionState: {
  162. readyState: WebSocket.OPEN,
  163. forceDisconnected: false,
  164. inactiveDisconnect: false,
  165. reconnectAt: null,
  166. forcedDisconnectDelay: 0,
  167. lastConnectionAttempt: 0,
  168. error: '',
  169. },
  170. isConnected: true,
  171. isStillReconnecting: false,
  172. secondsUntilReconnect: () => 0,
  173. tryReconnectNow: () => {},
  174. registerUserActivity: () => {},
  175. disconnect: () => {},
  176. }))
  177. return (
  178. <ConnectionContext.Provider value={value}>
  179. {children}
  180. </ConnectionContext.Provider>
  181. )
  182. }
  183. return ConnectionProvider
  184. }
  185. const makeIdeReactProvider = (scope, socket) => {
  186. const IdeReactProvider = ({ children }) => {
  187. const [startedFreeTrial, setStartedFreeTrial] = useState(false)
  188. const [ideReactContextValue] = useState(() => ({
  189. projectId: PROJECT_ID,
  190. eventEmitter: new IdeEventEmitter(),
  191. startedFreeTrial,
  192. setStartedFreeTrial,
  193. reportError: () => {},
  194. projectJoined: true,
  195. }))
  196. const [ideContextValue] = useState(() => {
  197. const scopeStore = createReactScopeValueStore(PROJECT_ID)
  198. for (const [key, value] of Object.entries(scope)) {
  199. // TODO: path for nested entries
  200. scopeStore.set(key, value)
  201. }
  202. const scopeEventEmitter = new ReactScopeEventEmitter(
  203. new IdeEventEmitter()
  204. )
  205. const unstableStore = new ReactScopeValueStore()
  206. return {
  207. socket,
  208. scopeStore,
  209. scopeEventEmitter,
  210. unstableStore,
  211. }
  212. })
  213. useEffect(() => {
  214. window.overleaf = {
  215. ...window.overleaf,
  216. unstable: {
  217. ...window.overleaf?.unstable,
  218. store: ideContextValue.unstableStore,
  219. },
  220. }
  221. }, [ideContextValue.unstableStore])
  222. return (
  223. <IdeReactContext.Provider value={ideReactContextValue}>
  224. <IdeContext.Provider value={ideContextValue}>
  225. {children}
  226. </IdeContext.Provider>
  227. </IdeReactContext.Provider>
  228. )
  229. }
  230. return IdeReactProvider
  231. }
  232. export function makeEditorOpenDocProvider(initialValues) {
  233. const {
  234. currentDocumentId: initialCurrentDocumentId,
  235. openDocName: initialOpenDocName,
  236. currentDocument: initialCurrentDocument,
  237. } = initialValues
  238. const EditorOpenDocProvider = ({ children }) => {
  239. const [currentDocumentId, setCurrentDocumentId] = useExposedState(
  240. initialCurrentDocumentId,
  241. 'editor.open_doc_id'
  242. )
  243. const [openDocName, setOpenDocName] = useExposedState(
  244. initialOpenDocName,
  245. 'editor.open_doc_name'
  246. )
  247. const [currentDocument, setCurrentDocument] = useState(
  248. initialCurrentDocument
  249. )
  250. const value = {
  251. currentDocumentId,
  252. setCurrentDocumentId,
  253. openDocName,
  254. setOpenDocName,
  255. currentDocument,
  256. setCurrentDocument,
  257. }
  258. return (
  259. <EditorOpenDocContext.Provider value={value}>
  260. {children}
  261. </EditorOpenDocContext.Provider>
  262. )
  263. }
  264. return EditorOpenDocProvider
  265. }
  266. const makeLayoutProvider = layoutContextOverrides => {
  267. const layout = {
  268. ...layoutContextDefault,
  269. ...layoutContextOverrides,
  270. }
  271. const LayoutProvider = ({ children }) => {
  272. const [view, setView] = useState(layout.view)
  273. const [openFile, setOpenFile] = useState(layout.openFile)
  274. const [chatIsOpen, setChatIsOpen] = useState(layout.chatIsOpen)
  275. const [reviewPanelOpen, setReviewPanelOpen] = useState(
  276. layout.reviewPanelOpen
  277. )
  278. const [miniReviewPanelVisible, setMiniReviewPanelVisible] = useState(
  279. layout.miniReviewPanelVisible
  280. )
  281. const [leftMenuShown, setLeftMenuShown] = useState(layout.leftMenuShown)
  282. const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(
  283. layout.projectSearchIsOpen
  284. )
  285. const [pdfLayout, setPdfLayout] = useState(layout.pdfLayout)
  286. const [loadingStyleSheet, setLoadingStyleSheet] = useState(
  287. layout.loadingStyleSheet
  288. )
  289. useEventListener(
  290. 'ui.toggle-review-panel',
  291. useCallback(() => {
  292. setReviewPanelOpen(open => !open)
  293. }, [setReviewPanelOpen])
  294. )
  295. const changeLayout = useCallback(
  296. (newLayout, newView = 'editor') => {
  297. setPdfLayout(newLayout)
  298. setView(newLayout === 'sideBySide' ? 'editor' : newView)
  299. },
  300. [setPdfLayout, setView]
  301. )
  302. const {
  303. reattach,
  304. detach,
  305. isLinked: detachIsLinked,
  306. role: detachRole,
  307. } = useDetachLayout()
  308. const pdfPreviewOpen =
  309. pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher'
  310. const value = useMemo(
  311. () => ({
  312. reattach,
  313. detach,
  314. detachIsLinked,
  315. detachRole,
  316. changeLayout,
  317. chatIsOpen,
  318. leftMenuShown,
  319. openFile,
  320. pdfLayout,
  321. pdfPreviewOpen,
  322. projectSearchIsOpen,
  323. setProjectSearchIsOpen,
  324. reviewPanelOpen,
  325. miniReviewPanelVisible,
  326. loadingStyleSheet,
  327. setChatIsOpen,
  328. setLeftMenuShown,
  329. setOpenFile,
  330. setPdfLayout,
  331. setReviewPanelOpen,
  332. setMiniReviewPanelVisible,
  333. setLoadingStyleSheet,
  334. setView,
  335. view,
  336. }),
  337. [
  338. reattach,
  339. detach,
  340. detachIsLinked,
  341. detachRole,
  342. changeLayout,
  343. chatIsOpen,
  344. leftMenuShown,
  345. openFile,
  346. pdfLayout,
  347. pdfPreviewOpen,
  348. projectSearchIsOpen,
  349. setProjectSearchIsOpen,
  350. reviewPanelOpen,
  351. miniReviewPanelVisible,
  352. loadingStyleSheet,
  353. setChatIsOpen,
  354. setLeftMenuShown,
  355. setOpenFile,
  356. setPdfLayout,
  357. setReviewPanelOpen,
  358. setMiniReviewPanelVisible,
  359. setLoadingStyleSheet,
  360. setView,
  361. view,
  362. ]
  363. )
  364. return (
  365. <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
  366. )
  367. }
  368. return LayoutProvider
  369. }