editor-providers.tsx 23 KB

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