editor-providers.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  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, {
  7. useCallback,
  8. useEffect,
  9. useState,
  10. useMemo,
  11. type FC,
  12. type PropsWithChildren,
  13. } from 'react'
  14. import { IdeReactContext } from '@/features/ide-react/context/ide-react-context'
  15. import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
  16. import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
  17. import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
  18. import { ConnectionContext } from '@/features/ide-react/context/connection-context'
  19. import {
  20. EditorOpenDocContext,
  21. type EditorOpenDocContextState,
  22. } from '@/features/ide-react/context/editor-open-doc-context'
  23. import { ProjectContext } from '@/shared/context/project-context'
  24. import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
  25. import useEventListener from '@/shared/hooks/use-event-listener'
  26. import useDetachLayout from '@/shared/hooks/use-detach-layout'
  27. import useExposedState from '@/shared/hooks/use-exposed-state'
  28. import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
  29. import {
  30. EditorPropertiesContext,
  31. EditorPropertiesContextValue,
  32. } from '@/features/ide-react/context/editor-properties-context'
  33. import {
  34. type IdeLayout,
  35. type IdeView,
  36. LayoutContext,
  37. type LayoutContextValue,
  38. } from '@/shared/context/layout-context'
  39. import type { Socket } from '@/features/ide-react/connection/types/socket'
  40. import type { PermissionsLevel } from '@/features/ide-react/types/permissions'
  41. import type { Folder } from '../../../types/folder'
  42. import type { SocketDebuggingInfo } from '@/features/ide-react/connection/types/connection-state'
  43. import type { DocumentContainer } from '@/features/ide-react/editor/document-container'
  44. import {
  45. ProjectMetadata,
  46. ProjectUpdate,
  47. } from '@/shared/context/types/project-metadata'
  48. import { User, UserId } from '../../../types/user'
  49. import { ProjectCompiler } from '../../../types/project-settings'
  50. import { ReferencesContext } from '@/features/ide-react/context/references-context'
  51. import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
  52. import { defaultSettings } from '@/shared/context/user-settings-context'
  53. import { UserSettings } from '@ol-types/user-settings'
  54. import { DetachCompileContext } from '@/shared/context/detach-compile-context'
  55. import { type CompileContext } from '@/shared/context/local-compile-context'
  56. import { EditorContext } from '@/shared/context/editor-context'
  57. import { Cobranding } from '@ol-types/cobranding'
  58. import { EDITOR_SESSION_ID } from '@/features/pdf-preview/util/metrics'
  59. // these constants can be imported in tests instead of
  60. // using magic strings
  61. export const PROJECT_ID = 'project123'
  62. export const PROJECT_NAME = 'project-name'
  63. export const USER_ID = '123abd' as UserId
  64. export const USER_EMAIL = 'testuser@example.com'
  65. const defaultUserSettings = {
  66. ...defaultSettings,
  67. referencesSearchMode: 'simple',
  68. } satisfies UserSettings
  69. export type EditorProvidersProps = {
  70. user?: Pick<
  71. User,
  72. | 'id'
  73. | 'email'
  74. | 'signUpDate'
  75. | 'activeProfessionalGroupSubscriptions'
  76. | 'isProfessionalGroupPlan'
  77. >
  78. projectId?: string
  79. projectName?: string
  80. projectOwner?: ProjectMetadata['owner']
  81. rootDocId?: string
  82. imageName?: string
  83. compiler?: ProjectCompiler
  84. socket?: Socket
  85. isRestrictedTokenMember?: boolean
  86. scope?: Record<string, any>
  87. features?: Record<string, boolean>
  88. projectFeatures?: Record<string, boolean>
  89. permissionsLevel?: PermissionsLevel
  90. children?: React.ReactNode
  91. rootFolder?: Folder[]
  92. layoutContext?: Partial<LayoutContextValue>
  93. userSettings?: Record<string, any>
  94. providers?: Record<string, React.FC<React.PropsWithChildren<any>>>
  95. mockCompileOnLoad?: boolean
  96. }
  97. export const projectDefaults: ProjectMetadata = {
  98. _id: PROJECT_ID,
  99. name: PROJECT_NAME,
  100. owner: {
  101. _id: '124abd' as UserId,
  102. email: 'owner@example.com',
  103. first_name: 'Test',
  104. last_name: 'Owner',
  105. privileges: 'owner',
  106. signUpDate: new Date('2025-07-07').toISOString(),
  107. },
  108. features: {
  109. referencesSearch: true,
  110. gitBridge: false,
  111. },
  112. rootDocId: '_root_doc_id',
  113. rootFolder: [
  114. {
  115. _id: 'root-folder-id',
  116. name: 'rootFolder',
  117. docs: [
  118. {
  119. _id: '_root_doc_id',
  120. name: 'main.tex',
  121. },
  122. ],
  123. folders: [],
  124. fileRefs: [],
  125. },
  126. ],
  127. imageName: 'texlive-full:2024.1',
  128. compiler: 'pdflatex' as ProjectCompiler,
  129. members: [],
  130. invites: [],
  131. trackChangesState: {} as Record<UserId | '__guests__', boolean>,
  132. spellCheckLanguage: 'en',
  133. }
  134. /**
  135. * @typedef {import('@/shared/context/layout-context').LayoutContextValue} LayoutContextValue
  136. * @type Partial<LayoutContextValue>
  137. */
  138. const layoutContextDefault = {
  139. view: 'editor',
  140. openFile: null,
  141. chatIsOpen: true, // false in the application, true in tests
  142. reviewPanelOpen: false,
  143. miniReviewPanelVisible: false,
  144. settingsShown: false,
  145. projectSearchIsOpen: false,
  146. pdfLayout: 'sideBySide',
  147. loadingStyleSheet: false,
  148. } satisfies Partial<LayoutContextValue>
  149. export function EditorProviders({
  150. user = {
  151. id: USER_ID,
  152. email: USER_EMAIL,
  153. signUpDate: '2025-10-10T10:10:10Z',
  154. },
  155. projectId = projectDefaults._id,
  156. projectName = projectDefaults.name,
  157. projectOwner = projectDefaults.owner,
  158. rootDocId = projectDefaults.rootDocId,
  159. imageName = projectDefaults.imageName,
  160. compiler = projectDefaults.compiler,
  161. socket = new SocketIOMock() as any as Socket,
  162. isRestrictedTokenMember = false,
  163. scope: defaultScope = {},
  164. features = {
  165. referencesSearch: true,
  166. gitBridge: false,
  167. },
  168. projectFeatures = features,
  169. permissionsLevel = 'owner',
  170. children,
  171. rootFolder = projectDefaults.rootFolder,
  172. /** @type {Partial<LayoutContext>} */
  173. layoutContext = layoutContextDefault,
  174. userSettings = {},
  175. providers = {},
  176. mockCompileOnLoad = false,
  177. }: EditorProvidersProps) {
  178. window.metaAttributesCache.set(
  179. 'ol-gitBridgePublicBaseUrl',
  180. 'https://git.overleaf.test'
  181. )
  182. window.metaAttributesCache.set(
  183. 'ol-isRestrictedTokenMember',
  184. isRestrictedTokenMember
  185. )
  186. window.metaAttributesCache.set(
  187. 'ol-userSettings',
  188. merge({}, defaultUserSettings, userSettings)
  189. )
  190. window.metaAttributesCache.set('ol-capabilities', [
  191. 'chat',
  192. 'dropbox',
  193. 'link-sharing',
  194. ])
  195. window.metaAttributesCache.set('ol-defaultLatexCompiler', 'pdflatex')
  196. const scope = merge(
  197. {
  198. user,
  199. editor: {
  200. sharejs_doc: {
  201. doc_id: 'test-doc',
  202. getSnapshot: () => 'some doc content',
  203. hasBufferedOps: () => false,
  204. on: () => {},
  205. off: () => {},
  206. leaveAndCleanUpPromise: async () => {},
  207. } as any as DocumentContainer,
  208. openDocName: null,
  209. currentDocumentId: null,
  210. wantTrackChanges: false,
  211. },
  212. permissionsLevel,
  213. },
  214. defaultScope
  215. )
  216. const project = {
  217. _id: projectId,
  218. name: projectName,
  219. owner: projectOwner,
  220. features: projectFeatures,
  221. rootDocId,
  222. rootFolder,
  223. imageName,
  224. compiler,
  225. members: [],
  226. invites: [],
  227. trackChangesState: false,
  228. spellCheckLanguage: 'en',
  229. }
  230. // Add details for useUserContext
  231. window.metaAttributesCache.set('ol-user', { ...user, features })
  232. window.metaAttributesCache.set('ol-project_id', projectId)
  233. const customProviders: Record<string, FC<PropsWithChildren>> = {
  234. ConnectionProvider: makeConnectionProvider(socket),
  235. IdeReactProvider: makeIdeReactProvider(scope, socket),
  236. EditorOpenDocProvider: makeEditorOpenDocProvider({
  237. currentDocumentId: scope.editor.currentDocumentId,
  238. openDocName: scope.editor.openDocName,
  239. currentDocument: scope.editor.sharejs_doc,
  240. }),
  241. EditorPropertiesProvider: makeEditorPropertiesProvider({
  242. wantTrackChanges: scope.editor.wantTrackChanges,
  243. }),
  244. LayoutProvider: makeLayoutProvider(layoutContext),
  245. ProjectProvider: makeProjectProvider(project),
  246. ReferencesProvider: makeReferencesProvider(),
  247. ...providers,
  248. }
  249. // Only use the mock EditorProvider when explicitly required
  250. if (providers.EditorProvider) {
  251. customProviders.EditorProvider = providers.EditorProvider
  252. }
  253. // Only override DetachCompileProvider when we need the mock
  254. if (mockCompileOnLoad) {
  255. customProviders.DetachCompileProvider =
  256. makeDetachCompileProvider(mockCompileOnLoad)
  257. }
  258. // Otherwise, let ReactContextRoot use the real DetachCompileProvider from production
  259. return (
  260. <ReactContextRoot providers={customProviders}>{children}</ReactContextRoot>
  261. )
  262. }
  263. export function makeEditorProvider({
  264. isProjectOwner = true,
  265. cobranding = undefined,
  266. renameProject = () => {},
  267. isRestrictedTokenMember,
  268. hasSuggestionsLeft = false,
  269. hasTokensLeft = false,
  270. premiumSuggestionResetDate = new Date(),
  271. tokenResetDate = new Date(),
  272. }: {
  273. isProjectOwner?: boolean
  274. cobranding?: Cobranding
  275. renameProject?: () => void
  276. isRestrictedTokenMember?: boolean
  277. hasSuggestionsLeft?: boolean
  278. hasTokensLeft?: boolean
  279. premiumSuggestionResetDate?: Date
  280. tokenResetDate?: Date
  281. } = {}) {
  282. const EditorProvider: FC<PropsWithChildren> = ({ children }) => {
  283. const value = {
  284. isProjectOwner,
  285. renameProject,
  286. isPendingEditor: false,
  287. hasSuggestionsLeft,
  288. premiumSuggestionResetDate,
  289. hasTokensLeft,
  290. tokensLeft: 0,
  291. setTokensLeft: () => {},
  292. tokenResetDate,
  293. setTokenResetDate: () => {},
  294. suggestionsLeft: 0,
  295. setSuggestionsLeft: () => {},
  296. setPremiumSuggestionResetDate: () => {},
  297. writefullInstance: null,
  298. setWritefullInstance: () => {},
  299. cobranding,
  300. isRestrictedTokenMember,
  301. upgradeTrackChangesModal: { show: false },
  302. setUpgradeTrackChangesModal: () => {},
  303. }
  304. return (
  305. <EditorContext.Provider value={value}>{children}</EditorContext.Provider>
  306. )
  307. }
  308. return EditorProvider
  309. }
  310. const makeReferencesProvider = () => {
  311. const ReferencesProvider: FC<PropsWithChildren> = ({ children }) => {
  312. return (
  313. <ReferencesContext.Provider
  314. value={{
  315. referenceKeys: new Set(),
  316. indexAllReferences: () => Promise.resolve(),
  317. searchLocalReferences() {
  318. return Promise.resolve({
  319. hits: [],
  320. })
  321. },
  322. }}
  323. >
  324. {children}
  325. </ReferencesContext.Provider>
  326. )
  327. }
  328. return ReferencesProvider
  329. }
  330. const makeConnectionProvider = (socket: Socket) => {
  331. const ConnectionProvider: FC<PropsWithChildren> = ({ children }) => {
  332. const [value] = useState(() => ({
  333. socket,
  334. connectionState: {
  335. readyState: WebSocket.OPEN,
  336. forceDisconnected: false,
  337. inactiveDisconnect: false,
  338. reconnectAt: null,
  339. forcedDisconnectDelay: 0,
  340. lastConnectionAttempt: 0,
  341. error: '' as const,
  342. },
  343. isConnected: true,
  344. isStillReconnecting: false,
  345. secondsUntilReconnect: () => 0,
  346. tryReconnectNow: () => {},
  347. registerUserActivity: () => {},
  348. disconnect: () => {},
  349. closeConnection: () => {},
  350. getSocketDebuggingInfo: () => ({}) as SocketDebuggingInfo,
  351. }))
  352. return (
  353. <ConnectionContext.Provider value={value}>
  354. {children}
  355. </ConnectionContext.Provider>
  356. )
  357. }
  358. return ConnectionProvider
  359. }
  360. const makeIdeReactProvider = (
  361. scope: Record<string, unknown>,
  362. socket: Socket
  363. ) => {
  364. const IdeReactProvider: FC<PropsWithChildren> = ({ children }) => {
  365. const [startedFreeTrial, setStartedFreeTrial] = useState(false)
  366. const [ideReactContextValue] = useState(() => ({
  367. projectId: PROJECT_ID,
  368. eventEmitter: new IdeEventEmitter(),
  369. startedFreeTrial,
  370. setStartedFreeTrial,
  371. reportError: () => {},
  372. projectJoined: true,
  373. permissionsLevel: scope.permissionsLevel as PermissionsLevel,
  374. setPermissionsLevel: () => {},
  375. setOutOfSync: () => {},
  376. }))
  377. const [ideContextValue] = useState(() => {
  378. const scopeEventEmitter = new ReactScopeEventEmitter(
  379. new IdeEventEmitter()
  380. )
  381. const unstableStore = new ReactScopeValueStore()
  382. return {
  383. socket,
  384. scopeEventEmitter,
  385. unstableStore,
  386. }
  387. })
  388. useEffect(() => {
  389. window.overleaf = {
  390. ...window.overleaf,
  391. unstable: {
  392. ...window.overleaf?.unstable,
  393. store: ideContextValue.unstableStore,
  394. },
  395. }
  396. }, [ideContextValue.unstableStore])
  397. return (
  398. <IdeReactContext.Provider value={ideReactContextValue}>
  399. <IdeContext.Provider value={ideContextValue}>
  400. {children}
  401. </IdeContext.Provider>
  402. </IdeReactContext.Provider>
  403. )
  404. }
  405. return IdeReactProvider
  406. }
  407. export function makeEditorOpenDocProvider(
  408. initialValues: EditorOpenDocContextState
  409. ) {
  410. const {
  411. currentDocumentId: initialCurrentDocumentId,
  412. openDocName: initialOpenDocName,
  413. currentDocument: initialCurrentDocument,
  414. } = initialValues
  415. const EditorOpenDocProvider: FC<PropsWithChildren> = ({ children }) => {
  416. const [currentDocumentId, setCurrentDocumentId] = useExposedState(
  417. initialCurrentDocumentId,
  418. 'editor.open_doc_id'
  419. )
  420. const [openDocName, setOpenDocName] = useExposedState(
  421. initialOpenDocName,
  422. 'editor.open_doc_name'
  423. )
  424. const [currentDocument, setCurrentDocument] = useState(
  425. initialCurrentDocument
  426. )
  427. const value = {
  428. currentDocumentId,
  429. setCurrentDocumentId,
  430. openDocName,
  431. setOpenDocName,
  432. currentDocument,
  433. setCurrentDocument,
  434. }
  435. return (
  436. <EditorOpenDocContext.Provider value={value}>
  437. {children}
  438. </EditorOpenDocContext.Provider>
  439. )
  440. }
  441. return EditorOpenDocProvider
  442. }
  443. const makeLayoutProvider = (
  444. layoutContextOverrides?: Partial<LayoutContextValue>
  445. ) => {
  446. const layout = {
  447. ...layoutContextDefault,
  448. ...layoutContextOverrides,
  449. }
  450. const LayoutProvider: FC<PropsWithChildren> = ({ children }) => {
  451. const [view, setView] = useState<IdeView | null>(layout.view)
  452. const [openFile, setOpenFile] = useState(layout.openFile)
  453. const [chatIsOpen, setChatIsOpen] = useState(layout.chatIsOpen)
  454. const [reviewPanelOpen, setReviewPanelOpen] = useState(
  455. layout.reviewPanelOpen
  456. )
  457. const [miniReviewPanelVisible, setMiniReviewPanelVisible] = useState(
  458. layout.miniReviewPanelVisible
  459. )
  460. const [settingsShown, setSettingsShown] = useState(layout.settingsShown)
  461. const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(
  462. layout.projectSearchIsOpen
  463. )
  464. const [pdfLayout, setPdfLayout] = useState(layout.pdfLayout)
  465. const [loadingStyleSheet, setLoadingStyleSheet] = useState(
  466. layout.loadingStyleSheet
  467. )
  468. const { sendEvent } = useEditorAnalytics()
  469. useEventListener(
  470. 'ui.toggle-review-panel',
  471. useCallback(() => {
  472. setReviewPanelOpen(open => !open)
  473. }, [setReviewPanelOpen])
  474. )
  475. const changeLayout = useCallback(
  476. (newLayout: IdeLayout, newView: IdeView = 'editor') => {
  477. setPdfLayout(newLayout)
  478. setView(newLayout === 'sideBySide' ? 'editor' : newView)
  479. },
  480. [setPdfLayout, setView]
  481. )
  482. const restoreView = useCallback(() => {
  483. setView('editor')
  484. }, [])
  485. const {
  486. reattach,
  487. detach,
  488. isLinked: detachIsLinked,
  489. role: detachRole,
  490. } = useDetachLayout()
  491. const handleDetach = useCallback(() => {
  492. detach()
  493. sendEvent('project-layout-detach')
  494. }, [detach, sendEvent])
  495. const handleReattach = useCallback(() => {
  496. if (detachRole !== 'detacher') {
  497. return
  498. }
  499. reattach()
  500. sendEvent('project-layout-reattach')
  501. }, [detachRole, reattach, sendEvent])
  502. const handleChangeLayout = useCallback(
  503. (newLayout: IdeLayout, newView?: IdeView) => {
  504. handleReattach()
  505. changeLayout(newLayout, newView)
  506. sendEvent('project-layout-change', {
  507. layout: newLayout,
  508. view: newView,
  509. })
  510. },
  511. [changeLayout, handleReattach, sendEvent]
  512. )
  513. const pdfPreviewOpen =
  514. pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher'
  515. const value = useMemo(
  516. () => ({
  517. reattach,
  518. detach,
  519. detachIsLinked,
  520. detachRole,
  521. changeLayout,
  522. chatIsOpen,
  523. settingsShown,
  524. openFile,
  525. pdfLayout,
  526. pdfPreviewOpen,
  527. projectSearchIsOpen,
  528. setProjectSearchIsOpen,
  529. reviewPanelOpen,
  530. miniReviewPanelVisible,
  531. loadingStyleSheet,
  532. setChatIsOpen,
  533. setSettingsShown,
  534. setOpenFile,
  535. setPdfLayout,
  536. setReviewPanelOpen,
  537. setMiniReviewPanelVisible,
  538. setLoadingStyleSheet,
  539. setView,
  540. view,
  541. restoreView,
  542. handleChangeLayout,
  543. handleDetach,
  544. focusMode: layout.focusMode ?? false,
  545. setFocusMode: layout.setFocusMode ?? (() => {}),
  546. }),
  547. [
  548. reattach,
  549. detach,
  550. detachIsLinked,
  551. detachRole,
  552. changeLayout,
  553. chatIsOpen,
  554. settingsShown,
  555. openFile,
  556. pdfLayout,
  557. pdfPreviewOpen,
  558. projectSearchIsOpen,
  559. setProjectSearchIsOpen,
  560. reviewPanelOpen,
  561. miniReviewPanelVisible,
  562. loadingStyleSheet,
  563. setChatIsOpen,
  564. setSettingsShown,
  565. setOpenFile,
  566. setPdfLayout,
  567. setReviewPanelOpen,
  568. setMiniReviewPanelVisible,
  569. setLoadingStyleSheet,
  570. setView,
  571. view,
  572. restoreView,
  573. handleChangeLayout,
  574. handleDetach,
  575. ]
  576. )
  577. return (
  578. <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
  579. )
  580. }
  581. return LayoutProvider
  582. }
  583. export function makeEditorPropertiesProvider(
  584. initialValues: Partial<
  585. Pick<
  586. EditorPropertiesContextValue,
  587. 'showVisual' | 'showSymbolPalette' | 'wantTrackChanges'
  588. >
  589. >
  590. ) {
  591. const EditorPropertiesProvider: FC<PropsWithChildren> = ({ children }) => {
  592. const {
  593. showVisual: initialShowVisual,
  594. showSymbolPalette: initialShowSymbolPalette,
  595. wantTrackChanges: initialWantTrackChanges,
  596. } = initialValues
  597. const [showVisual, setShowVisual] = useState(initialShowVisual || false)
  598. const [showSymbolPalette, setShowSymbolPalette] = useState(
  599. initialShowSymbolPalette || false
  600. )
  601. function toggleSymbolPalette() {
  602. setShowSymbolPalette(show => !show)
  603. }
  604. const [opening, setOpening] = useState(true)
  605. const [trackChanges, setTrackChanges] = useState(false)
  606. const [wantTrackChanges, setWantTrackChanges] = useState(
  607. initialWantTrackChanges || false
  608. )
  609. const [errorState, setErrorState] = useState(false)
  610. const value = {
  611. showVisual,
  612. setShowVisual,
  613. showVisualForFile: () => showVisual,
  614. showSymbolPalette,
  615. setShowSymbolPalette,
  616. toggleSymbolPalette,
  617. opening,
  618. setOpening,
  619. trackChanges,
  620. setTrackChanges,
  621. wantTrackChanges,
  622. setWantTrackChanges,
  623. errorState,
  624. setErrorState,
  625. }
  626. return (
  627. <EditorPropertiesContext.Provider value={value}>
  628. {children}
  629. </EditorPropertiesContext.Provider>
  630. )
  631. }
  632. return EditorPropertiesProvider
  633. }
  634. export function makeProjectProvider(initialProject: ProjectMetadata) {
  635. const ProjectProvider: FC<PropsWithChildren> = ({ children }) => {
  636. const [project, setProject] = useState(initialProject)
  637. const updateProject = useCallback((projectUpdateData: ProjectUpdate) => {
  638. setProject(projectData =>
  639. Object.assign({}, projectData, projectUpdateData)
  640. )
  641. }, [])
  642. const value = {
  643. projectId: project._id,
  644. project,
  645. joinProject: () => {},
  646. updateProject,
  647. joinedOnce: true,
  648. projectSnapshot: new ProjectSnapshot(project._id),
  649. tags: [],
  650. features: project.features,
  651. name: project.name,
  652. }
  653. return (
  654. <ProjectContext.Provider value={value}>
  655. {children}
  656. </ProjectContext.Provider>
  657. )
  658. }
  659. return ProjectProvider
  660. }
  661. const BASE_COMPILE_CONTEXT_MOCK = {
  662. animateCompileDropdownArrow: false,
  663. clearCache: () => {},
  664. clearingCache: false,
  665. clsiServerId: undefined,
  666. codeCheckFailed: false,
  667. deliveryLatencies: {},
  668. editedSinceCompileStarted: false,
  669. fileList: undefined,
  670. hasChanges: false,
  671. hasShortCompileTimeout: false,
  672. highlights: undefined,
  673. isProjectOwner: true,
  674. lastCompileOptions: {},
  675. logEntryAnnotations: undefined,
  676. logEntries: undefined,
  677. outputFilesArchive: undefined,
  678. pdfViewer: 'pdfjs',
  679. position: undefined,
  680. rawLog: undefined,
  681. recompileFromScratch: () => {},
  682. setAnimateCompileDropdownArrow: () => {},
  683. setHasLintingError: () => {},
  684. setHighlights: () => {},
  685. setPosition: () => {},
  686. setShowCompileTimeWarning: () => {},
  687. setShowLogs: () => {},
  688. toggleLogs: () => {},
  689. setStopOnValidationError: () => {},
  690. showLogs: false,
  691. showCompileTimeWarning: false,
  692. stopCompile: () => {},
  693. stopOnValidationError: true,
  694. stoppedOnFirstError: false,
  695. uncompiled: false,
  696. validationIssues: undefined,
  697. firstRenderDone: () => {},
  698. setChangedAt: () => {},
  699. cleanupCompileResult: undefined,
  700. syncToEntry: () => {},
  701. recordAction: () => {},
  702. darkModePdf: false,
  703. setDarkModePdf: () => {},
  704. activeOverallTheme: 'light',
  705. } as const
  706. const makeDetachCompileProvider = (mockCompileOnLoad: boolean = false) => {
  707. const DetachCompileProvider: FC<PropsWithChildren> = ({ children }) => {
  708. const [pdfUrl, setPdfUrl] = useState<string | undefined>()
  709. const [pdfDownloadUrl, setPdfDownloadUrl] = useState<string | undefined>()
  710. const [pdfFile, setPdfFile] = useState<any>()
  711. const [compiling, setCompiling] = useState(false)
  712. const [error, setError] = useState<string | undefined>()
  713. const [autoCompile, setAutoCompile] = useState(true)
  714. const [draft, setDraft] = useState(false)
  715. const [stopOnFirstError, setStopOnFirstError] = useState(false)
  716. const startCompile = useCallback(async () => {
  717. setCompiling(true)
  718. try {
  719. const response = await fetch('/project/123abc/compile', {
  720. method: 'POST',
  721. })
  722. const data = await response.json()
  723. const pdfFileData = data.outputFiles?.find(
  724. (file: any) => file.type === 'pdf'
  725. )
  726. if (data.status === 'success' && pdfFileData) {
  727. const newPdfUrl = `${data.pdfDownloadDomain || ''}${pdfFileData.url}`
  728. const params = [
  729. data.compileGroup && `compileGroup=${data.compileGroup}`,
  730. data.clsiServerId && `clsiserverid=${data.clsiServerId}`,
  731. `editorId=${EDITOR_SESSION_ID}`,
  732. 'popupDownload=true',
  733. ]
  734. .filter(Boolean)
  735. .join('&')
  736. const newPdfDownloadUrl = `/download/project/123abc/build/${pdfFileData.build}/output/${pdfFileData.path}?${params}`
  737. setPdfUrl(newPdfUrl)
  738. setPdfDownloadUrl(newPdfDownloadUrl)
  739. setPdfFile({ pdfUrl: newPdfUrl, pdfDownloadUrl: newPdfDownloadUrl })
  740. }
  741. } catch (err) {
  742. setError('Compile failed')
  743. } finally {
  744. setCompiling(false)
  745. }
  746. }, [])
  747. useEffect(() => {
  748. if (mockCompileOnLoad) {
  749. startCompile()
  750. }
  751. // eslint-disable-next-line react-hooks/exhaustive-deps
  752. }, [startCompile])
  753. const value = {
  754. ...BASE_COMPILE_CONTEXT_MOCK,
  755. autoCompile,
  756. compiling,
  757. draft,
  758. error,
  759. pdfDownloadUrl,
  760. pdfFile,
  761. pdfUrl,
  762. setAutoCompile,
  763. setCompiling,
  764. setDraft,
  765. setError,
  766. setStopOnFirstError,
  767. startCompile,
  768. stopOnFirstError,
  769. } as CompileContext
  770. return (
  771. <DetachCompileContext.Provider value={value}>
  772. {children}
  773. </DetachCompileContext.Provider>
  774. )
  775. }
  776. return DetachCompileProvider
  777. }