editor-providers.tsx 23 KB

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