editor-providers.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. // Disable prop type checks for test harnesses
  2. /* eslint-disable react/prop-types */
  3. import sinon from 'sinon'
  4. import { get, merge } from 'lodash'
  5. import { SplitTestProvider } from '@/shared/context/split-test-context'
  6. import { UserProvider } from '@/shared/context/user-context'
  7. import { ProjectProvider } from '@/shared/context/project-context'
  8. import { FileTreeDataProvider } from '@/shared/context/file-tree-data-context'
  9. import { EditorProvider } from '@/shared/context/editor-context'
  10. import { DetachProvider } from '@/shared/context/detach-context'
  11. import { LayoutProvider } from '@/shared/context/layout-context'
  12. import { LocalCompileProvider } from '@/shared/context/local-compile-context'
  13. import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
  14. import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context'
  15. import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
  16. import { UserSettingsProvider } from '@/shared/context/user-settings-context'
  17. import { OutlineProvider } from '@/features/ide-react/context/outline-context'
  18. import { SocketIOMock } from '@/ide/connection/SocketIoShim'
  19. import { IdeContext } from '@/shared/context/ide-context'
  20. import React, { useEffect, useState } from 'react'
  21. import {
  22. createReactScopeValueStore,
  23. IdeReactContext,
  24. } from '@/features/ide-react/context/ide-react-context'
  25. import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
  26. import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
  27. import { ConnectionContext } from '@/features/ide-react/context/connection-context'
  28. import { EventLog } from '@/features/ide-react/editor/event-log'
  29. import { ChatProvider } from '@/features/chat/context/chat-context'
  30. import { EditorManagerProvider } from '@/features/ide-react/context/editor-manager-context'
  31. import { FileTreeOpenProvider } from '@/features/ide-react/context/file-tree-open-context'
  32. import { MetadataProvider } from '@/features/ide-react/context/metadata-context'
  33. import { ModalsContextProvider } from '@/features/ide-react/context/modals-context'
  34. import { OnlineUsersProvider } from '@/features/ide-react/context/online-users-context'
  35. import { PermissionsProvider } from '@/features/ide-react/context/permissions-context'
  36. import { ReferencesProvider } from '@/features/ide-react/context/references-context'
  37. import { SnapshotProvider } from '@/features/ide-react/context/snapshot-context'
  38. import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
  39. // these constants can be imported in tests instead of
  40. // using magic strings
  41. export const PROJECT_ID = 'project123'
  42. export const PROJECT_NAME = 'project-name'
  43. export const USER_ID = '123abd'
  44. export const USER_EMAIL = 'testuser@example.com'
  45. const defaultUserSettings = {
  46. pdfViewer: 'pdfjs',
  47. fontSize: 12,
  48. fontFamily: 'monaco',
  49. lineHeight: 'normal',
  50. editorTheme: 'textmate',
  51. overallTheme: '',
  52. mode: 'default',
  53. autoComplete: true,
  54. autoPairDelimiters: true,
  55. trackChanges: true,
  56. syntaxValidation: false,
  57. mathPreview: true,
  58. }
  59. export function EditorProviders({
  60. user = { id: USER_ID, email: USER_EMAIL },
  61. projectId = PROJECT_ID,
  62. projectOwner = {
  63. _id: '124abd',
  64. email: 'owner@example.com',
  65. },
  66. rootDocId = '_root_doc_id',
  67. socket = new SocketIOMock(),
  68. isRestrictedTokenMember = false,
  69. clsiServerId = '1234',
  70. scope = {},
  71. features = {
  72. referencesSearch: true,
  73. },
  74. permissionsLevel = 'owner',
  75. children,
  76. rootFolder = [
  77. {
  78. _id: 'root-folder-id',
  79. name: 'rootFolder',
  80. docs: [],
  81. folders: [],
  82. fileRefs: [],
  83. },
  84. ],
  85. ui = { view: 'editor', pdfLayout: 'sideBySide', chatOpen: true },
  86. fileTreeManager = {
  87. findEntityById: () => null,
  88. findEntityByPath: () => null,
  89. getEntityPath: () => '',
  90. getRootDocDirname: () => '',
  91. getPreviewByPath: path => ({ url: path, extension: 'png' }),
  92. },
  93. editorManager = {
  94. getCurrentDocId: () => 'foo',
  95. getCurrentDocValue: () => {},
  96. openDoc: sinon.stub(),
  97. },
  98. userSettings = {},
  99. providers = {},
  100. }) {
  101. window.metaAttributesCache.set(
  102. 'ol-gitBridgePublicBaseUrl',
  103. 'https://git.overleaf.test'
  104. )
  105. window.metaAttributesCache.set(
  106. 'ol-isRestrictedTokenMember',
  107. isRestrictedTokenMember
  108. )
  109. window.metaAttributesCache.set(
  110. 'ol-userSettings',
  111. merge({}, defaultUserSettings, userSettings)
  112. )
  113. const $scope = merge(
  114. {
  115. user,
  116. editor: {
  117. sharejs_doc: {
  118. doc_id: 'test-doc',
  119. getSnapshot: () => 'some doc content',
  120. },
  121. },
  122. project: {
  123. _id: projectId,
  124. name: PROJECT_NAME,
  125. owner: projectOwner,
  126. features,
  127. rootDoc_id: rootDocId,
  128. rootFolder,
  129. },
  130. ui,
  131. $watch: (path, callback) => {
  132. callback(get($scope, path))
  133. return () => null
  134. },
  135. $on: sinon.stub(),
  136. $applyAsync: sinon.stub(),
  137. permissionsLevel,
  138. },
  139. scope
  140. )
  141. window._ide = {
  142. $scope,
  143. socket,
  144. clsiServerId,
  145. editorManager,
  146. fileTreeManager,
  147. }
  148. // Add details for useUserContext
  149. window.metaAttributesCache.set('ol-user', { ...user, features })
  150. window.metaAttributesCache.set('ol-project_id', projectId)
  151. const Providers = {
  152. ChatProvider,
  153. ConnectionProvider,
  154. DetachCompileProvider,
  155. DetachProvider,
  156. EditorProvider,
  157. EditorLeftMenuProvider,
  158. EditorManagerProvider,
  159. SnapshotProvider,
  160. FileTreeDataProvider,
  161. FileTreeOpenProvider,
  162. FileTreePathProvider,
  163. IdeReactProvider,
  164. LayoutProvider,
  165. LocalCompileProvider,
  166. MetadataProvider,
  167. ModalsContextProvider,
  168. OnlineUsersProvider,
  169. OutlineProvider,
  170. PermissionsProvider,
  171. ProjectProvider,
  172. ProjectSettingsProvider,
  173. ReferencesProvider,
  174. SplitTestProvider,
  175. UserProvider,
  176. UserSettingsProvider,
  177. ...providers,
  178. }
  179. return (
  180. <Providers.SplitTestProvider>
  181. <Providers.ModalsContextProvider>
  182. <Providers.ConnectionProvider>
  183. <Providers.IdeReactProvider>
  184. <Providers.UserProvider>
  185. <Providers.UserSettingsProvider>
  186. <Providers.ProjectProvider>
  187. <Providers.SnapshotProvider>
  188. <Providers.FileTreeDataProvider>
  189. <Providers.FileTreePathProvider>
  190. <Providers.ReferencesProvider>
  191. <Providers.DetachProvider>
  192. <Providers.EditorProvider>
  193. <Providers.PermissionsProvider>
  194. <Providers.ProjectSettingsProvider>
  195. <Providers.LayoutProvider>
  196. <Providers.EditorManagerProvider>
  197. <Providers.LocalCompileProvider>
  198. <Providers.DetachCompileProvider>
  199. <Providers.ChatProvider>
  200. <Providers.FileTreeOpenProvider>
  201. <Providers.OnlineUsersProvider>
  202. <Providers.MetadataProvider>
  203. <Providers.OutlineProvider>
  204. <Providers.EditorLeftMenuProvider>
  205. {children}
  206. </Providers.EditorLeftMenuProvider>
  207. </Providers.OutlineProvider>
  208. </Providers.MetadataProvider>
  209. </Providers.OnlineUsersProvider>
  210. </Providers.FileTreeOpenProvider>
  211. </Providers.ChatProvider>
  212. </Providers.DetachCompileProvider>
  213. </Providers.LocalCompileProvider>
  214. </Providers.EditorManagerProvider>
  215. </Providers.LayoutProvider>
  216. </Providers.ProjectSettingsProvider>
  217. </Providers.PermissionsProvider>
  218. </Providers.EditorProvider>
  219. </Providers.DetachProvider>
  220. </Providers.ReferencesProvider>
  221. </Providers.FileTreePathProvider>
  222. </Providers.FileTreeDataProvider>
  223. </Providers.SnapshotProvider>
  224. </Providers.ProjectProvider>
  225. </Providers.UserSettingsProvider>
  226. </Providers.UserProvider>
  227. </Providers.IdeReactProvider>
  228. </Providers.ConnectionProvider>
  229. </Providers.ModalsContextProvider>
  230. </Providers.SplitTestProvider>
  231. )
  232. }
  233. const ConnectionProvider = ({ children }) => {
  234. const [value] = useState(() => ({
  235. socket: window._ide.socket,
  236. connectionState: {
  237. readyState: WebSocket.OPEN,
  238. forceDisconnected: false,
  239. inactiveDisconnect: false,
  240. reconnectAt: null,
  241. forcedDisconnectDelay: 0,
  242. lastConnectionAttempt: 0,
  243. error: '',
  244. },
  245. isConnected: true,
  246. isStillReconnecting: false,
  247. secondsUntilReconnect: () => 0,
  248. tryReconnectNow: () => {},
  249. registerUserActivity: () => {},
  250. disconnect: () => {},
  251. }))
  252. return (
  253. <ConnectionContext.Provider value={value}>
  254. {children}
  255. </ConnectionContext.Provider>
  256. )
  257. }
  258. const IdeReactProvider = ({ children }) => {
  259. const [startedFreeTrial, setStartedFreeTrial] = useState(false)
  260. const [ideReactContextValue] = useState(() => ({
  261. projectId: PROJECT_ID,
  262. eventEmitter: new IdeEventEmitter(),
  263. eventLog: new EventLog(),
  264. startedFreeTrial,
  265. setStartedFreeTrial,
  266. reportError: () => {},
  267. projectJoined: true,
  268. }))
  269. const [ideContextValue] = useState(() => {
  270. const ide = window._ide
  271. const scopeStore = createReactScopeValueStore(PROJECT_ID)
  272. for (const [key, value] of Object.entries(ide.$scope)) {
  273. // TODO: path for nested entries
  274. scopeStore.set(key, value)
  275. }
  276. scopeStore.set('editor.sharejs_doc', ide.$scope.editor.sharejs_doc)
  277. scopeStore.set('ui.chatOpen', ide.$scope.ui.chatOpen)
  278. const scopeEventEmitter = new ReactScopeEventEmitter(new IdeEventEmitter())
  279. return {
  280. ...ide,
  281. scopeStore,
  282. scopeEventEmitter,
  283. }
  284. })
  285. useEffect(() => {
  286. window.overleaf = {
  287. ...window.overleaf,
  288. unstable: {
  289. ...window.overleaf?.unstable,
  290. store: ideContextValue.scopeStore,
  291. },
  292. }
  293. }, [ideContextValue.scopeStore])
  294. return (
  295. <IdeReactContext.Provider value={ideReactContextValue}>
  296. <IdeContext.Provider value={ideContextValue}>
  297. {children}
  298. </IdeContext.Provider>
  299. </IdeReactContext.Provider>
  300. )
  301. }