editor-providers.tsx 24 KB

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