Przeglądaj źródła

Handle PDF preview on toggle between split and full-width views (#5470)

* Only hide the compile logs pane when toggled off
* Handle PDF preview on toggle between split and full-width views

GitOrigin-RevId: 9ceca8a06a22abfa78f245e1ae5d24af98215906
Alf Eaton 4 lat temu
rodzic
commit
f7ef2532e0
22 zmienionych plików z 500 dodań i 642 usunięć
  1. 3 6
      services/web/app/views/project/editor/editor.pug
  2. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.js
  3. 3 3
      services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.js
  4. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-download-button.js
  5. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.js
  6. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.js
  7. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-code-check-button.js
  8. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.js
  9. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.js
  10. 17 37
      services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js
  11. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-logs-button.js
  12. 5 3
      services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js
  13. 1 4
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js
  14. 1 6
      services/web/frontend/js/features/pdf-preview/components/pdf-preview.js
  15. 2 2
      services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js
  16. 0 494
      services/web/frontend/js/features/pdf-preview/contexts/pdf-preview-context.js
  17. 0 4
      services/web/frontend/js/features/pdf-preview/util/compiler.js
  18. 17 0
      services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.js
  19. 398 19
      services/web/frontend/js/shared/context/compile-context.js
  20. 13 5
      services/web/frontend/js/shared/context/layout-context.js
  21. 4 4
      services/web/frontend/js/shared/context/root-context.js
  22. 20 39
      services/web/frontend/stories/pdf-preview.stories.js

+ 3 - 6
services/web/app/views/project/editor/editor.pug

@@ -18,13 +18,10 @@ div.full-size(
 		include ./editor-no-symbol-palette
 
 	.ui-layout-east
-		// The pdf-preview component needs to always be rendered, even when the editor is in "full-width" mode and it's not visible. 
-		// It doesn't recompile while hidden, due to the ui.pdfHidden flag, but maintains its state for when it's shown again.
-		if showNewPdfPreview
-			div(ng-show="ui.pdfLayout == 'sideBySide'")
+		div(ng-if="ui.pdfLayout == 'sideBySide'")
+			if showNewPdfPreview
 				pdf-preview()
-		else
-			div(ng-if="ui.pdfLayout == 'sideBySide'")
+			else
 				include ./pdf
 
 	.ui-layout-resizer-controls.synctex-controls(

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.js

@@ -1,11 +1,11 @@
 import Icon from '../../../shared/components/icon'
 import { Button } from 'react-bootstrap'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { useTranslation } from 'react-i18next'
 import { memo } from 'react'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfClearCacheButton() {
-  const { compiling, clearCache, clearingCache } = usePdfPreviewContext()
+  const { compiling, clearCache, clearingCache } = useCompileContext()
 
   const { t } = useTranslation()
 

+ 3 - 3
services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.js

@@ -8,9 +8,9 @@ import {
 import Icon from '../../../shared/components/icon'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import { useTranslation } from 'react-i18next'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { memo } from 'react'
 import classnames from 'classnames'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
 
@@ -23,11 +23,11 @@ function PdfCompileButton() {
     setAutoCompile,
     setDraft,
     setStopOnValidationError,
+    stopOnValidationError,
     startCompile,
     stopCompile,
-    stopOnValidationError,
     recompileFromScratch,
-  } = usePdfPreviewContext()
+  } = useCompileContext()
 
   const { t } = useTranslation()
 

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-download-button.js

@@ -3,11 +3,11 @@ import { Button, Dropdown } from 'react-bootstrap'
 import Icon from '../../../shared/components/icon'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import PdfFileList from './pdf-file-list'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { memo } from 'react'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfDownloadButton() {
-  const { compiling, pdfDownloadUrl, fileList } = usePdfPreviewContext()
+  const { compiling, pdfDownloadUrl, fileList } = useCompileContext()
 
   const { t } = useTranslation()
 

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.js

@@ -2,11 +2,11 @@ import { Dropdown } from 'react-bootstrap'
 import PdfFileList from './pdf-file-list'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import { memo } from 'react'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { useTranslation } from 'react-i18next'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfDownloadFilesButton() {
-  const { compiling, fileList } = usePdfPreviewContext()
+  const { compiling, fileList } = useCompileContext()
 
   const { t } = useTranslation()
 

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.js

@@ -1,11 +1,11 @@
 import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import Icon from '../../../shared/components/icon'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { memo, useMemo } from 'react'
+import { useLayoutContext } from '../../../shared/context/layout-context'
 
 function PdfExpandButton() {
-  const { pdfLayout, switchLayout } = usePdfPreviewContext()
+  const { pdfLayout, switchLayout } = useLayoutContext()
 
   const { t } = useTranslation()
 

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-code-check-button.js

@@ -1,12 +1,12 @@
 import { memo, useCallback } from 'react'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { sendMBOnce } from '../../../infrastructure/event-tracking'
 import { Button } from 'react-bootstrap'
 import Icon from '../../../shared/components/icon'
 import { useTranslation } from 'react-i18next'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfHybridCodeCheckButton() {
-  const { codeCheckFailed, error, setShowLogs } = usePdfPreviewContext()
+  const { codeCheckFailed, error, setShowLogs } = useCompileContext()
 
   const { t } = useTranslation()
 

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.js

@@ -1,11 +1,11 @@
 import { useTranslation } from 'react-i18next'
 import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
 import Icon from '../../../shared/components/icon'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { memo } from 'react'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfHybridDownloadButton() {
-  const { pdfDownloadUrl } = usePdfPreviewContext()
+  const { pdfDownloadUrl } = useCompileContext()
 
   const { t } = useTranslation()
 

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.js

@@ -1,12 +1,12 @@
 import { memo, useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
 import { Button, Label, OverlayTrigger, Tooltip } from 'react-bootstrap'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { sendMBOnce } from '../../../infrastructure/event-tracking'
 import Icon from '../../../shared/components/icon'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfHybridLogsButton() {
-  const { error, logEntries, setShowLogs, showLogs } = usePdfPreviewContext()
+  const { error, logEntries, setShowLogs, showLogs } = useCompileContext()
 
   const { t } = useTranslation()
 

+ 17 - 37
services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js

@@ -19,10 +19,6 @@ function PdfJsViewer({ url }) {
     `pdf-viewer-scale:${projectId}`,
     'page-width'
   )
-  const [, setScrollTop] = usePersistedState(
-    `pdf-viewer-scroll-top:${projectId}`,
-    0
-  )
 
   // state values shared with Angular scope (highlights => editor, position => synctex buttons
   const [highlights] = useScopeValue('pdf.highlights')
@@ -63,23 +59,10 @@ function PdfJsViewer({ url }) {
     }
   }, [pdfJsWrapper, url])
 
-  useEffect(() => {
-    if (pdfJsWrapper) {
-      // listen for 'pdf:scroll-to-position' events
-      const eventListener = event => {
-        pdfJsWrapper.container.scrollTop = event.data.position
-      }
-
-      window.addEventListener('pdf:scroll-to-position', eventListener)
-
-      return () => {
-        window.removeEventListener('pdf:scroll-to-position', eventListener)
-      }
-    }
-  }, [pdfJsWrapper])
-
   // listen for scroll events
   useEffect(() => {
+    let storePositionTimer
+
     if (initialised && pdfJsWrapper) {
       // store the scroll position in localStorage, for the synctex button
       const storePosition = debounce(pdfViewer => {
@@ -87,33 +70,30 @@ function PdfJsViewer({ url }) {
         try {
           setPosition(pdfViewer.currentPosition)
         } catch (error) {
-          // TODO
-          console.error(error)
+          // TODO: investigate handling missing offsetParent in jsdom
+          // console.error(error)
         }
       }, 500)
 
-      // store the scroll position in localStorage, for use when reloading
-      const storeScrollTop = debounce(pdfViewer => {
-        // set position for "sync to code" button
-        setScrollTop(pdfViewer.container.scrollTop)
-      }, 500)
-
-      storePosition(pdfJsWrapper)
+      storePositionTimer = window.setTimeout(() => {
+        storePosition(pdfJsWrapper)
+      }, 100)
 
       const scrollListener = () => {
-        storeScrollTop(pdfJsWrapper)
         storePosition(pdfJsWrapper)
       }
 
       pdfJsWrapper.container.addEventListener('scroll', scrollListener)
 
       return () => {
-        storePosition.cancel()
-        storeScrollTop.cancel()
         pdfJsWrapper.container.removeEventListener('scroll', scrollListener)
+        if (storePositionTimer) {
+          window.clearTimeout(storePositionTimer)
+        }
+        storePosition.cancel()
       }
     }
-  }, [setPosition, setScrollTop, pdfJsWrapper, initialised])
+  }, [setPosition, pdfJsWrapper, initialised])
 
   // listen for double-click events
   useEffect(() => {
@@ -147,14 +127,14 @@ function PdfJsViewer({ url }) {
       })
 
       // restore the scroll position
-      setScrollTop(scrollTop => {
-        if (scrollTop > 0) {
-          pdfJsWrapper.container.scrollTop = scrollTop
+      setPosition(position => {
+        if (position) {
+          pdfJsWrapper.currentPosition = position
         }
-        return scrollTop
+        return position
       })
     }
-  }, [initialised, setScale, setScrollTop, pdfJsWrapper])
+  }, [initialised, setScale, setPosition, pdfJsWrapper])
 
   // transmit scale value to the viewer when it changes
   useEffect(() => {

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-logs-button.js

@@ -1,8 +1,8 @@
 import { memo, useCallback, useMemo } from 'react'
 import { Button } from 'react-bootstrap'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { PdfLogsButtonContent } from './pdf-logs-button-content'
 import { sendMBOnce } from '../../../infrastructure/event-tracking'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfLogsButton() {
   const {
@@ -11,7 +11,7 @@ function PdfLogsButton() {
     logEntries,
     showLogs,
     setShowLogs,
-  } = usePdfPreviewContext()
+  } = useCompileContext()
 
   const buttonStyle = useMemo(() => {
     if (showLogs) {

+ 5 - 3
services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js

@@ -1,7 +1,7 @@
 import { useTranslation } from 'react-i18next'
 import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { memo } from 'react'
+import classnames from 'classnames'
 import PdfValidationIssue from './pdf-validation-issue'
 import TimeoutUpgradePrompt from './timeout-upgrade-prompt'
 import PdfPreviewError from './pdf-preview-error'
@@ -12,6 +12,7 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
 import ErrorBoundaryFallback from './error-boundary-fallback'
 import PdfCodeCheckFailedNotice from '../../preview/components/pdf-code-check-failed-notice'
 import PdfLogsPaneInfoNotice from '../../preview/components/pdf-logs-pane-info-notice'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 function PdfLogsViewer() {
   const {
@@ -20,12 +21,13 @@ function PdfLogsViewer() {
     logEntries,
     rawLog,
     validationIssues,
-  } = usePdfPreviewContext()
+    showLogs,
+  } = useCompileContext()
 
   const { t } = useTranslation()
 
   return (
-    <div className="logs-pane">
+    <div className={classnames('logs-pane', { hidden: !showLogs })}>
       <div className="logs-pane-content">
         <PdfLogsPaneInfoNotice />
 

+ 1 - 4
services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js

@@ -1,7 +1,6 @@
 import { memo, Suspense } from 'react'
 import PdfLogsViewer from './pdf-logs-viewer'
 import PdfViewer from './pdf-viewer'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import LoadingSpinner from '../../../shared/components/loading-spinner'
 import PdfHybridPreviewToolbar from './pdf-preview-hybrid-toolbar'
 import PdfPreviewToolbar from './pdf-preview-toolbar'
@@ -11,8 +10,6 @@ const newPreviewToolbar = new URLSearchParams(window.location.search).has(
 )
 
 function PdfPreviewPane() {
-  const { showLogs } = usePdfPreviewContext()
-
   return (
     <div className="pdf full-size">
       {newPreviewToolbar ? <PdfPreviewToolbar /> : <PdfHybridPreviewToolbar />}
@@ -21,7 +18,7 @@ function PdfPreviewPane() {
           <PdfViewer />
         </div>
       </Suspense>
-      {showLogs && <PdfLogsViewer />}
+      <PdfLogsViewer />
     </div>
   )
 }

+ 1 - 6
services/web/frontend/js/features/pdf-preview/components/pdf-preview.js

@@ -1,15 +1,10 @@
-import PdfPreviewProvider from '../contexts/pdf-preview-context'
 import PdfPreviewPane from './pdf-preview-pane'
 import { memo } from 'react'
 import withErrorBoundary from '../../../infrastructure/error-boundary'
 import ErrorBoundaryFallback from './error-boundary-fallback'
 
 function PdfPreview() {
-  return (
-    <PdfPreviewProvider>
-      <PdfPreviewPane />
-    </PdfPreviewProvider>
-  )
+  return <PdfPreviewPane />
 }
 
 export default withErrorBoundary(memo(PdfPreview), () => (

+ 2 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js

@@ -1,6 +1,6 @@
 import useScopeValue from '../../../shared/hooks/use-scope-value'
-import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { lazy, memo, useEffect } from 'react'
+import { useCompileContext } from '../../../shared/context/compile-context'
 
 const PdfJsViewer = lazy(() =>
   import(/* webpackChunkName: "pdf-js-viewer" */ './pdf-js-viewer')
@@ -19,7 +19,7 @@ function PdfViewer() {
     }
   }, [setPdfViewer])
 
-  const { pdfUrl } = usePdfPreviewContext()
+  const { pdfUrl } = useCompileContext()
 
   if (!pdfUrl) {
     return null

+ 0 - 494
services/web/frontend/js/features/pdf-preview/contexts/pdf-preview-context.js

@@ -1,494 +0,0 @@
-import {
-  createContext,
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useState,
-} from 'react'
-import PropTypes from 'prop-types'
-import useScopeValue from '../../../shared/hooks/use-scope-value'
-import { useProjectContext } from '../../../shared/context/project-context'
-import usePersistedState from '../../../shared/hooks/use-persisted-state'
-import {
-  buildLogEntryAnnotations,
-  handleOutputFiles,
-} from '../util/output-files'
-import {
-  send,
-  sendMB,
-  sendMBSampled,
-} from '../../../infrastructure/event-tracking'
-import { useEditorContext } from '../../../shared/context/editor-context'
-import useAbortController from '../../../shared/hooks/use-abort-controller'
-import DocumentCompiler from '../util/compiler'
-import { useIdeContext } from '../../../shared/context/ide-context'
-import { useLayoutContext } from '../../../shared/context/layout-context'
-import { useCompileContext } from '../../../shared/context/compile-context'
-
-export const PdfPreviewContext = createContext(undefined)
-
-PdfPreviewProvider.propTypes = {
-  children: PropTypes.any,
-}
-
-export default function PdfPreviewProvider({ children }) {
-  const ide = useIdeContext()
-
-  const { pdfHidden, pdfLayout, setPdfLayout, setView } = useLayoutContext()
-
-  const project = useProjectContext()
-
-  const projectId = project._id
-
-  const { hasPremiumCompile, isProjectOwner } = useEditorContext()
-
-  const {
-    logEntries,
-    pdfDownloadUrl,
-    pdfUrl,
-    setClsiServerId,
-    setLogEntries,
-    setLogEntryAnnotations,
-    setPdfDownloadUrl,
-    setPdfUrl,
-    setUncompiled,
-    uncompiled,
-  } = useCompileContext()
-
-  // whether a compile is in progress
-  const [compiling, setCompiling] = useState(false)
-
-  // data received in response to a compile request
-  const [data, setData] = useState()
-
-  // whether the project has been compiled yet
-  const [compiledOnce, setCompiledOnce] = useState(false)
-
-  // whether the cache is being cleared
-  const [clearingCache, setClearingCache] = useState(false)
-
-  // whether the logs should be visible
-  const [showLogs, setShowLogs] = useState(false)
-
-  // an error that occurred
-  const [error, setError] = useState()
-
-  // the list of files that can be downloaded
-  const [fileList, setFileList] = useState()
-
-  // the raw contents of the log file
-  const [rawLog, setRawLog] = useState()
-
-  // validation issues from CLSI
-  const [validationIssues, setValidationIssues] = useState()
-
-  // whether autocompile is switched on
-  const [autoCompile, _setAutoCompile] = usePersistedState(
-    `autocompile_enabled:${projectId}`,
-    false,
-    true
-  )
-
-  // whether the compile should run in draft mode
-  const [draft, setDraft] = usePersistedState(`draft:${projectId}`, false, true)
-
-  // whether compiling should be prevented if there are linting errors
-  const [stopOnValidationError, setStopOnValidationError] = usePersistedState(
-    `stop_on_validation_error:${projectId}`,
-    true,
-    true
-  )
-
-  // the Document currently open in the editor
-  const [currentDoc] = useScopeValue('editor.sharejs_doc')
-
-  // whether the editor linter found errors
-  const [hasLintingError, setHasLintingError] = useScopeValue('hasLintingError')
-
-  // whether syntax validation is enabled globally
-  const [syntaxValidation] = useScopeValue('settings.syntaxValidation')
-
-  // the timestamp that a doc was last changed or saved
-  const [changedAt, setChangedAt] = useState(0)
-
-  const { signal } = useAbortController()
-
-  // the document compiler
-  const [compiler] = useState(() => {
-    return new DocumentCompiler({
-      project,
-      setChangedAt,
-      setCompiling,
-      setData,
-      setError,
-      signal,
-    })
-  })
-
-  // clean up the compiler on unmount
-  useEffect(() => {
-    return () => {
-      compiler.destroy()
-    }
-  }, [compiler])
-
-  // keep currentDoc in sync with the compiler
-  useEffect(() => {
-    compiler.currentDoc = currentDoc
-  }, [compiler, currentDoc])
-
-  // keep draft setting in sync with the compiler
-  useEffect(() => {
-    compiler.draft = draft
-  }, [compiler, draft])
-
-  // pass the "uncompiled" value up into the scope for use outside this context provider
-  useEffect(() => {
-    setUncompiled(changedAt > 0)
-  }, [setUncompiled, changedAt])
-
-  // record changes to the autocompile setting
-  const setAutoCompile = useCallback(
-    value => {
-      _setAutoCompile(value)
-      sendMB('autocompile-setting-changed', { value })
-    },
-    [_setAutoCompile]
-  )
-
-  // always compile the PDF once after opening the project, after the doc has loaded
-  useEffect(() => {
-    if (!compiledOnce && currentDoc) {
-      setCompiledOnce(true)
-      compiler.compile({ isAutoCompileOnLoad: true })
-    }
-  }, [compiledOnce, currentDoc, compiler])
-
-  // handle the data returned from a compile request
-  // note: this should _only_ run when `data` changes,
-  // the other dependencies must all be static
-  useEffect(() => {
-    if (data) {
-      if (data.clsiServerId) {
-        setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController
-        compiler.clsiServerId = data.clsiServerId
-      }
-
-      if (data.compileGroup) {
-        compiler.compileGroup = data.compileGroup
-      }
-
-      if (data.outputFiles) {
-        handleOutputFiles(projectId, data).then(result => {
-          setLogEntryAnnotations(
-            buildLogEntryAnnotations(result.logEntries.all, ide.fileTreeManager)
-          )
-          setLogEntries(result.logEntries)
-          setFileList(result.fileList)
-          setPdfDownloadUrl(result.pdfDownloadUrl)
-          setPdfUrl(result.pdfUrl)
-          setRawLog(result.log)
-
-          // sample compile stats for real users
-          if (!window.user.alphaProgram && data.status === 'success') {
-            sendMBSampled(
-              'compile-result',
-              {
-                errors: result.logEntries.errors.length,
-                warnings: result.logEntries.warnings.length,
-                typesetting: result.logEntries.typesetting.length,
-                newPdfPreview: true, // TODO: is this useful?
-              },
-              0.01
-            )
-          }
-        })
-      }
-
-      switch (data.status) {
-        case 'success':
-          setError(undefined)
-          setShowLogs(false)
-          break
-
-        case 'clsi-maintenance':
-        case 'compile-in-progress':
-        case 'exited':
-        case 'failure':
-        case 'project-too-large':
-        case 'terminated':
-        case 'too-recently-compiled':
-          setError(data.status)
-          break
-
-        case 'timedout':
-          setError('timedout')
-
-          if (!hasPremiumCompile && isProjectOwner) {
-            send(
-              'subscription-funnel',
-              'editor-click-feature',
-              'compile-timeout'
-            )
-            sendMB('paywall-prompt', {
-              'paywall-type': 'compile-timeout',
-            })
-          }
-          break
-
-        case 'autocompile-backoff':
-          if (!data.options.isAutoCompileOnLoad) {
-            setError('autocompile-disabled')
-            setAutoCompile(false)
-            sendMB('autocompile-rate-limited', { hasPremiumCompile })
-          }
-          break
-
-        case 'unavailable':
-          setError('clsi-unavailable')
-          break
-
-        case 'validation-problems':
-          setError('validation-problems')
-          setValidationIssues(data.validationProblems)
-          break
-
-        default:
-          setError('error')
-          break
-      }
-    }
-  }, [
-    compiler,
-    data,
-    ide,
-    hasPremiumCompile,
-    isProjectOwner,
-    projectId,
-    setAutoCompile,
-    setClsiServerId,
-    setLogEntries,
-    setLogEntryAnnotations,
-    setPdfDownloadUrl,
-    setPdfUrl,
-  ])
-
-  // switch to logs if there's an error
-  useEffect(() => {
-    if (error) {
-      setShowLogs(true)
-    }
-  }, [error])
-
-  // recompile on key press
-  useEffect(() => {
-    const listener = event => {
-      compiler.compile(event.detail)
-    }
-
-    window.addEventListener('pdf:recompile', listener)
-
-    return () => {
-      window.removeEventListener('pdf:recompile', listener)
-    }
-  }, [compiler])
-
-  // whether there has been an autocompile linting error, if syntax validation is switched on
-  const autoCompileLintingError = Boolean(
-    autoCompile && syntaxValidation && hasLintingError
-  )
-
-  const codeCheckFailed = stopOnValidationError && autoCompileLintingError
-
-  // show that the project has pending changes
-  const hasChanges = Boolean(
-    autoCompile && uncompiled && compiledOnce && !codeCheckFailed
-  )
-
-  // the project is available for auto-compiling
-  const canAutoCompile = Boolean(
-    autoCompile && !compiling && !pdfHidden && !codeCheckFailed
-  )
-
-  // call the debounced autocompile function if the project is available for auto-compiling and it has changed
-  useEffect(() => {
-    if (canAutoCompile && changedAt > 0) {
-      compiler.debouncedAutoCompile()
-    } else {
-      compiler.debouncedAutoCompile.cancel()
-    }
-  }, [compiler, canAutoCompile, changedAt])
-
-  // cancel debounced recompile on unmount
-  useEffect(() => {
-    return () => {
-      compiler.debouncedAutoCompile.cancel()
-    }
-  }, [compiler])
-
-  // record doc changes when notified by the editor
-  useEffect(() => {
-    const listener = () => {
-      setChangedAt(Date.now())
-    }
-
-    window.addEventListener('doc:changed', listener)
-    window.addEventListener('doc:saved', listener)
-
-    return () => {
-      window.removeEventListener('doc:changed', listener)
-      window.removeEventListener('doc:saved', listener)
-    }
-  }, [])
-
-  // start a compile manually
-  const startCompile = useCallback(() => {
-    compiler.compile()
-  }, [compiler])
-
-  // stop a compile manually
-  const stopCompile = useCallback(() => {
-    compiler.stopCompile()
-  }, [compiler])
-
-  // clear the compile cache
-  const clearCache = useCallback(() => {
-    setClearingCache(true)
-
-    return compiler.clearCache().finally(() => {
-      setClearingCache(false)
-    })
-  }, [compiler])
-
-  // clear the cache then run a compile, triggered by a menu item
-  const recompileFromScratch = useCallback(() => {
-    clearCache().then(() => {
-      compiler.compile()
-    })
-  }, [clearCache, compiler])
-
-  // switch to either side-by-side or flat (full-width) layout
-  // TODO: move this into LayoutContext?
-  const switchLayout = useCallback(() => {
-    setPdfLayout(layout => {
-      const newLayout = layout === 'sideBySide' ? 'flat' : 'sideBySide'
-      setView(newLayout === 'sideBySide' ? 'editor' : 'pdf')
-      setPdfLayout(newLayout)
-      window.localStorage.setItem('pdf.layout', newLayout)
-    })
-  }, [setPdfLayout, setView])
-
-  // the context value, memoized to minimize re-rendering
-  const value = useMemo(() => {
-    return {
-      autoCompile,
-      codeCheckFailed,
-      clearCache,
-      clearingCache,
-      compiling,
-      draft,
-      error,
-      fileList,
-      hasChanges,
-      hasLintingError,
-      logEntries,
-      pdfDownloadUrl,
-      pdfLayout,
-      pdfUrl,
-      rawLog,
-      recompileFromScratch,
-      setAutoCompile,
-      setDraft,
-      setHasLintingError, // only for stories
-      setShowLogs,
-      setStopOnValidationError,
-      showLogs,
-      startCompile,
-      stopCompile,
-      stopOnValidationError,
-      switchLayout,
-      uncompiled,
-      validationIssues,
-    }
-  }, [
-    autoCompile,
-    codeCheckFailed,
-    clearCache,
-    clearingCache,
-    compiling,
-    draft,
-    error,
-    fileList,
-    hasChanges,
-    hasLintingError,
-    logEntries,
-    pdfDownloadUrl,
-    pdfLayout,
-    pdfUrl,
-    rawLog,
-    recompileFromScratch,
-    setAutoCompile,
-    setDraft,
-    setHasLintingError, // only for stories
-    setStopOnValidationError,
-    showLogs,
-    startCompile,
-    stopCompile,
-    stopOnValidationError,
-    switchLayout,
-    uncompiled,
-    validationIssues,
-  ])
-
-  return (
-    <PdfPreviewContext.Provider value={value}>
-      {children}
-    </PdfPreviewContext.Provider>
-  )
-}
-
-PdfPreviewContext.Provider.propTypes = {
-  value: PropTypes.shape({
-    autoCompile: PropTypes.bool.isRequired,
-    clearCache: PropTypes.func.isRequired,
-    clearingCache: PropTypes.bool.isRequired,
-    codeCheckFailed: PropTypes.bool.isRequired,
-    compiling: PropTypes.bool.isRequired,
-    draft: PropTypes.bool.isRequired,
-    error: PropTypes.string,
-    fileList: PropTypes.object,
-    hasChanges: PropTypes.bool.isRequired,
-    hasLintingError: PropTypes.bool,
-    logEntries: PropTypes.object,
-    pdfDownloadUrl: PropTypes.string,
-    pdfLayout: PropTypes.string,
-    pdfUrl: PropTypes.string,
-    rawLog: PropTypes.string,
-    recompileFromScratch: PropTypes.func.isRequired,
-    setAutoCompile: PropTypes.func.isRequired,
-    setDraft: PropTypes.func.isRequired,
-    setHasLintingError: PropTypes.func.isRequired, // only for storybook
-    setShowLogs: PropTypes.func.isRequired,
-    setStopOnValidationError: PropTypes.func.isRequired,
-    showLogs: PropTypes.bool.isRequired,
-    startCompile: PropTypes.func.isRequired,
-    stopCompile: PropTypes.func.isRequired,
-    stopOnValidationError: PropTypes.bool.isRequired,
-    switchLayout: PropTypes.func.isRequired,
-    uncompiled: PropTypes.bool,
-    validationIssues: PropTypes.object,
-  }),
-}
-
-export function usePdfPreviewContext() {
-  const context = useContext(PdfPreviewContext)
-
-  if (!context) {
-    throw new Error(
-      'usePdfPreviewContext is only available inside PdfPreviewProvider'
-    )
-  }
-
-  return context
-}

+ 0 - 4
services/web/frontend/js/features/pdf-preview/util/compiler.js

@@ -45,10 +45,6 @@ export default class DocumentCompiler {
     )
   }
 
-  destroy() {
-    this.debouncedAutoCompile.cancel()
-  }
-
   // The main "compile" function.
   // Call this directly to run a compile now, otherwise call debouncedAutoCompile.
   async compile(options = {}) {

+ 17 - 0
services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.js

@@ -151,6 +151,23 @@ export default class PDFJSWrapper {
     }
   }
 
+  set currentPosition(position) {
+    const destArray = [
+      null,
+      {
+        name: 'XYZ', // 'XYZ' = scroll to the given coordinates
+      },
+      position.offset.left,
+      position.offset.top,
+      null,
+    ]
+
+    this.viewer.scrollPageIntoView({
+      pageNumber: position.page + 1,
+      destArray,
+    })
+  }
+
   abortDocumentLoading() {
     this.loadDocumentTask = undefined
   }

+ 398 - 19
services/web/frontend/js/shared/context/compile-context.js

@@ -1,27 +1,72 @@
-import { createContext, useContext, useMemo } from 'react'
+import {
+  createContext,
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react'
 import PropTypes from 'prop-types'
 import useScopeValue from '../hooks/use-scope-value'
+import usePersistedState from '../hooks/use-persisted-state'
+import useAbortController from '../hooks/use-abort-controller'
+import DocumentCompiler from '../../features/pdf-preview/util/compiler'
+import {
+  send,
+  sendMB,
+  sendMBSampled,
+} from '../../infrastructure/event-tracking'
+import {
+  buildLogEntryAnnotations,
+  handleOutputFiles,
+} from '../../features/pdf-preview/util/output-files'
+import { useIdeContext } from './ide-context'
+import { useProjectContext } from './project-context'
+import { useEditorContext } from './editor-context'
 
 export const CompileContext = createContext()
 
 CompileContext.Provider.propTypes = {
   value: PropTypes.shape({
+    autoCompile: PropTypes.bool.isRequired,
+    clearingCache: PropTypes.bool.isRequired,
     clsiServerId: PropTypes.string,
+    codeCheckFailed: PropTypes.bool.isRequired,
+    compiling: PropTypes.bool.isRequired,
+    draft: PropTypes.bool.isRequired,
+    error: PropTypes.string,
+    fileList: PropTypes.object,
+    hasChanges: PropTypes.bool.isRequired,
+    hasLintingError: PropTypes.bool,
     logEntries: PropTypes.object,
     logEntryAnnotations: PropTypes.object,
     pdfDownloadUrl: PropTypes.string,
     pdfUrl: PropTypes.string,
-    setClsiServerId: PropTypes.func.isRequired,
-    setLogEntries: PropTypes.func.isRequired,
-    setLogEntryAnnotations: PropTypes.func.isRequired,
-    setPdfDownloadUrl: PropTypes.func.isRequired,
-    setPdfUrl: PropTypes.func.isRequired,
-    setUncompiled: PropTypes.func.isRequired,
+    rawLog: PropTypes.string,
+    setAutoCompile: PropTypes.func.isRequired,
+    setDraft: PropTypes.func.isRequired,
+    setHasLintingError: PropTypes.func.isRequired, // only for storybook
+    setShowLogs: PropTypes.func.isRequired,
+    setStopOnValidationError: PropTypes.func.isRequired,
+    showLogs: PropTypes.bool.isRequired,
+    stopOnValidationError: PropTypes.bool.isRequired,
     uncompiled: PropTypes.bool,
+    validationIssues: PropTypes.object,
   }),
 }
 
 export function CompileProvider({ children }) {
+  const ide = useIdeContext()
+
+  const { hasPremiumCompile, isProjectOwner } = useEditorContext()
+
+  const project = useProjectContext()
+
+  const projectId = project._id
+
+  // whether a compile is in progress
+  const [compiling, setCompiling] = useState(false)
+
   // the log entries parsed from the compile output log
   const [logEntries, setLogEntries] = useScopeValue('pdf.logEntries')
 
@@ -42,34 +87,368 @@ export function CompileProvider({ children }) {
   // the id of the CLSI server which ran the compile
   const [clsiServerId, setClsiServerId] = useScopeValue('pdf.clsiServerId')
 
+  // data received in response to a compile request
+  const [data, setData] = useState()
+
+  // whether the project has been compiled yet
+  const [compiledOnce, setCompiledOnce] = useState(false)
+
+  // whether the cache is being cleared
+  const [clearingCache, setClearingCache] = useState(false)
+
+  // whether the logs should be visible
+  const [showLogs, setShowLogs] = useState(false)
+
+  // an error that occurred
+  const [error, setError] = useState()
+
+  // the list of files that can be downloaded
+  const [fileList, setFileList] = useState()
+
+  // the raw contents of the log file
+  const [rawLog, setRawLog] = useState()
+
+  // validation issues from CLSI
+  const [validationIssues, setValidationIssues] = useState()
+
+  // whether autocompile is switched on
+  const [autoCompile, _setAutoCompile] = usePersistedState(
+    `autocompile_enabled:${projectId}`,
+    false,
+    true
+  )
+
+  // whether the compile should run in draft mode
+  const [draft, setDraft] = usePersistedState(`draft:${projectId}`, false, true)
+
+  // whether compiling should be prevented if there are linting errors
+  const [stopOnValidationError, setStopOnValidationError] = usePersistedState(
+    `stop_on_validation_error:${projectId}`,
+    true,
+    true
+  )
+
+  // the Document currently open in the editor
+  const [currentDoc] = useScopeValue('editor.sharejs_doc')
+
+  // whether the editor linter found errors
+  const [hasLintingError, setHasLintingError] = useScopeValue('hasLintingError')
+
+  // whether syntax validation is enabled globally
+  const [syntaxValidation] = useScopeValue('settings.syntaxValidation')
+
+  // the timestamp that a doc was last changed or saved
+  const [changedAt, setChangedAt] = useState(0)
+
+  const { signal } = useAbortController()
+
+  // the document compiler
+  const [compiler] = useState(() => {
+    return new DocumentCompiler({
+      project,
+      setChangedAt,
+      setCompiling,
+      setData,
+      setError,
+      signal,
+    })
+  })
+
+  // keep currentDoc in sync with the compiler
+  useEffect(() => {
+    compiler.currentDoc = currentDoc
+  }, [compiler, currentDoc])
+
+  // keep draft setting in sync with the compiler
+  useEffect(() => {
+    compiler.draft = draft
+  }, [compiler, draft])
+
+  // pass the "uncompiled" value up into the scope for use outside this context provider
+  useEffect(() => {
+    setUncompiled(changedAt > 0)
+  }, [setUncompiled, changedAt])
+
+  // record changes to the autocompile setting
+  const setAutoCompile = useCallback(
+    value => {
+      _setAutoCompile(value)
+      sendMB('autocompile-setting-changed', { value })
+    },
+    [_setAutoCompile]
+  )
+
+  // always compile the PDF once after opening the project, after the doc has loaded
+  useEffect(() => {
+    if (!compiledOnce && currentDoc) {
+      setCompiledOnce(true)
+      compiler.compile({ isAutoCompileOnLoad: true })
+    }
+  }, [compiledOnce, currentDoc, compiler])
+
+  // handle the data returned from a compile request
+  // note: this should _only_ run when `data` changes,
+  // the other dependencies must all be static
+  useEffect(() => {
+    if (data) {
+      if (data.clsiServerId) {
+        setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController
+        compiler.clsiServerId = data.clsiServerId
+      }
+
+      if (data.compileGroup) {
+        compiler.compileGroup = data.compileGroup
+      }
+
+      if (data.outputFiles) {
+        handleOutputFiles(projectId, data).then(result => {
+          setLogEntryAnnotations(
+            buildLogEntryAnnotations(result.logEntries.all, ide.fileTreeManager)
+          )
+          setLogEntries(result.logEntries)
+          setFileList(result.fileList)
+          setPdfDownloadUrl(result.pdfDownloadUrl)
+          setPdfUrl(result.pdfUrl)
+          setRawLog(result.log)
+
+          // sample compile stats for real users
+          if (!window.user.alphaProgram && data.status === 'success') {
+            sendMBSampled(
+              'compile-result',
+              {
+                errors: result.logEntries.errors.length,
+                warnings: result.logEntries.warnings.length,
+                typesetting: result.logEntries.typesetting.length,
+                newPdfPreview: true, // TODO: is this useful?
+              },
+              0.01
+            )
+          }
+        })
+      }
+
+      switch (data.status) {
+        case 'success':
+          setError(undefined)
+          setShowLogs(false)
+          break
+
+        case 'clsi-maintenance':
+        case 'compile-in-progress':
+        case 'exited':
+        case 'failure':
+        case 'project-too-large':
+        case 'rate-limited':
+        case 'terminated':
+        case 'too-recently-compiled':
+          setError(data.status)
+          break
+
+        case 'timedout':
+          setError('timedout')
+
+          if (!hasPremiumCompile && isProjectOwner) {
+            send(
+              'subscription-funnel',
+              'editor-click-feature',
+              'compile-timeout'
+            )
+            sendMB('paywall-prompt', {
+              'paywall-type': 'compile-timeout',
+            })
+          }
+          break
+
+        case 'autocompile-backoff':
+          if (!data.options.isAutoCompileOnLoad) {
+            setError('autocompile-disabled')
+            setAutoCompile(false)
+            sendMB('autocompile-rate-limited', { hasPremiumCompile })
+          }
+          break
+
+        case 'unavailable':
+          setError('clsi-unavailable')
+          break
+
+        case 'validation-problems':
+          setError('validation-problems')
+          setValidationIssues(data.validationProblems)
+          break
+
+        default:
+          setError('error')
+          break
+      }
+    }
+  }, [
+    compiler,
+    data,
+    ide,
+    hasPremiumCompile,
+    isProjectOwner,
+    projectId,
+    setAutoCompile,
+    setClsiServerId,
+    setLogEntries,
+    setLogEntryAnnotations,
+    setPdfDownloadUrl,
+    setPdfUrl,
+  ])
+
+  // switch to logs if there's an error
+  useEffect(() => {
+    if (error) {
+      setShowLogs(true)
+    }
+  }, [error])
+
+  // recompile on key press
+  useEffect(() => {
+    const listener = event => {
+      compiler.compile(event.detail)
+    }
+
+    window.addEventListener('pdf:recompile', listener)
+
+    return () => {
+      window.removeEventListener('pdf:recompile', listener)
+    }
+  }, [compiler])
+
+  // whether there has been an autocompile linting error, if syntax validation is switched on
+  const autoCompileLintingError = Boolean(
+    autoCompile && syntaxValidation && hasLintingError
+  )
+
+  const codeCheckFailed = stopOnValidationError && autoCompileLintingError
+
+  // show that the project has pending changes
+  const hasChanges = Boolean(
+    autoCompile && uncompiled && compiledOnce && !codeCheckFailed
+  )
+
+  // the project is available for auto-compiling
+  const canAutoCompile = Boolean(autoCompile && !compiling && !codeCheckFailed)
+
+  // call the debounced autocompile function if the project is available for auto-compiling and it has changed
+  useEffect(() => {
+    if (canAutoCompile && changedAt > 0) {
+      compiler.debouncedAutoCompile()
+    } else {
+      compiler.debouncedAutoCompile.cancel()
+    }
+  }, [compiler, canAutoCompile, changedAt])
+
+  // cancel debounced recompile on unmount
+  useEffect(() => {
+    return () => {
+      compiler.debouncedAutoCompile.cancel()
+    }
+  }, [compiler])
+
+  // record doc changes when notified by the editor
+  useEffect(() => {
+    const listener = event => {
+      setChangedAt(Date.now())
+    }
+
+    window.addEventListener('doc:changed', listener)
+    window.addEventListener('doc:saved', listener)
+
+    return () => {
+      window.removeEventListener('doc:changed', listener)
+      window.removeEventListener('doc:saved', listener)
+    }
+  }, [])
+
+  // start a compile manually
+  const startCompile = useCallback(() => {
+    compiler.compile()
+  }, [compiler])
+
+  // stop a compile manually
+  const stopCompile = useCallback(() => {
+    compiler.stopCompile()
+  }, [compiler])
+
+  // clear the compile cache
+  const clearCache = useCallback(() => {
+    setClearingCache(true)
+
+    return compiler.clearCache().finally(() => {
+      setClearingCache(false)
+    })
+  }, [compiler, setClearingCache])
+
+  // clear the cache then run a compile, triggered by a menu item
+  const recompileFromScratch = useCallback(() => {
+    clearCache().then(() => {
+      compiler.compile()
+    })
+  }, [clearCache, compiler])
+
   const value = useMemo(
     () => ({
+      autoCompile,
+      clearCache,
+      clearingCache,
       clsiServerId,
+      codeCheckFailed,
+      compiling,
+      draft,
+      error,
+      fileList,
+      hasChanges,
+      hasLintingError,
       logEntries,
       logEntryAnnotations,
       pdfDownloadUrl,
       pdfUrl,
-      setClsiServerId,
-      setLogEntries,
-      setLogEntryAnnotations,
-      setPdfDownloadUrl,
-      setPdfUrl,
-      setUncompiled,
+      rawLog,
+      recompileFromScratch,
+      setAutoCompile,
+      setClearingCache,
+      setCompiling,
+      setDraft,
+      setHasLintingError, // only for stories
+      setShowLogs,
+      setStopOnValidationError,
+      showLogs,
+      startCompile,
+      stopCompile,
+      stopOnValidationError,
       uncompiled,
+      validationIssues,
     }),
     [
+      autoCompile,
+      clearCache,
+      clearingCache,
       clsiServerId,
+      codeCheckFailed,
+      compiling,
+      draft,
+      error,
+      fileList,
+      hasChanges,
+      hasLintingError,
       logEntries,
       logEntryAnnotations,
       pdfDownloadUrl,
       pdfUrl,
-      setClsiServerId,
-      setLogEntries,
-      setLogEntryAnnotations,
-      setPdfDownloadUrl,
-      setPdfUrl,
-      setUncompiled,
+      rawLog,
+      recompileFromScratch,
+      setAutoCompile,
+      setDraft,
+      setHasLintingError,
+      setStopOnValidationError,
+      showLogs,
+      startCompile,
+      stopCompile,
+      stopOnValidationError,
       uncompiled,
+      validationIssues,
     ]
   )
 

+ 13 - 5
services/web/frontend/js/shared/context/layout-context.js

@@ -2,6 +2,7 @@ import { createContext, useContext, useCallback, useMemo } from 'react'
 import PropTypes from 'prop-types'
 import useScopeValue from '../hooks/use-scope-value'
 import { useIdeContext } from './ide-context'
+import localStorage from '../../infrastructure/local-storage'
 
 export const LayoutContext = createContext()
 
@@ -15,7 +16,7 @@ LayoutContext.Provider.propTypes = {
     setReviewPanelOpen: PropTypes.func.isRequired,
     leftMenuShown: PropTypes.bool,
     setLeftMenuShown: PropTypes.func.isRequired,
-    pdfLayout: PropTypes.oneOf(['sideBySide', 'flat', 'split']).isRequired,
+    pdfLayout: PropTypes.oneOf(['sideBySide', 'flat']).isRequired,
   }).isRequired,
 }
 
@@ -53,14 +54,20 @@ export function LayoutProvider({ children }) {
   // whether to display the editor and preview side-by-side or full-width ("flat")
   const [pdfLayout, setPdfLayout] = useScopeValue('ui.pdfLayout')
 
-  // whether the PDF preview pane is hidden
-  const [pdfHidden] = useScopeValue('ui.pdfHidden')
+  // switch to either side-by-side or flat (full-width) layout
+  const switchLayout = useCallback(() => {
+    setPdfLayout(layout => {
+      const newLayout = layout === 'sideBySide' ? 'flat' : 'sideBySide'
+      setView(newLayout === 'sideBySide' ? 'editor' : 'pdf')
+      setPdfLayout(newLayout)
+      localStorage.setItem('pdf.layout', newLayout)
+    })
+  }, [setPdfLayout, setView])
 
   const value = useMemo(
     () => ({
       chatIsOpen,
       leftMenuShown,
-      pdfHidden,
       pdfLayout,
       reviewPanelOpen,
       setChatIsOpen,
@@ -68,12 +75,12 @@ export function LayoutProvider({ children }) {
       setPdfLayout,
       setReviewPanelOpen,
       setView,
+      switchLayout,
       view,
     }),
     [
       chatIsOpen,
       leftMenuShown,
-      pdfHidden,
       pdfLayout,
       reviewPanelOpen,
       setChatIsOpen,
@@ -81,6 +88,7 @@ export function LayoutProvider({ children }) {
       setPdfLayout,
       setReviewPanelOpen,
       setView,
+      switchLayout,
       view,
     ]
   )

+ 4 - 4
services/web/frontend/js/shared/context/root-context.js

@@ -17,11 +17,11 @@ export function ContextRoot({ children, ide, settings }) {
         <UserProvider>
           <ProjectProvider>
             <EditorProvider settings={settings}>
-              <CompileProvider>
-                <LayoutProvider>
+              <LayoutProvider>
+                <CompileProvider>
                   <ChatProvider>{children}</ChatProvider>
-                </LayoutProvider>
-              </CompileProvider>
+                </CompileProvider>
+              </LayoutProvider>
             </EditorProvider>
           </ProjectProvider>
         </UserProvider>

+ 20 - 39
services/web/frontend/stories/pdf-preview.stories.js

@@ -3,9 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
 import useFetchMock from './hooks/use-fetch-mock'
 import { setupContext } from './fixtures/context'
 import { Button } from 'react-bootstrap'
-import PdfPreviewProvider, {
-  usePdfPreviewContext,
-} from '../js/features/pdf-preview/contexts/pdf-preview-context'
 import PdfPreviewPane from '../js/features/pdf-preview/components/pdf-preview-pane'
 import PdfPreview from '../js/features/pdf-preview/components/pdf-preview'
 import PdfPreviewToolbar from '../js/features/pdf-preview/components/pdf-preview-toolbar'
@@ -15,6 +12,7 @@ import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer
 import examplePdf from './fixtures/storybook-example.pdf'
 import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
 import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
+import { useCompileContext } from '../js/shared/context/compile-context'
 
 setupContext()
 
@@ -230,7 +228,7 @@ export const Interactive = () => {
   }, [])
 
   const Inner = () => {
-    const context = usePdfPreviewContext()
+    const context = useCompileContext()
 
     const { setHasLintingError } = context
 
@@ -369,10 +367,8 @@ export const Interactive = () => {
 
   return withContextRoot(
     <div className="pdf-viewer">
-      <PdfPreviewProvider>
-        <PdfPreviewPane />
-        <Inner />
-      </PdfPreviewProvider>
+      <PdfPreviewPane />
+      <Inner />
     </div>,
     scope
   )
@@ -406,7 +402,7 @@ export const CompileError = () => {
   })
 
   const Inner = () => {
-    const { startCompile } = usePdfPreviewContext()
+    const { startCompile } = useCompileContext()
 
     const handleStatusChange = useCallback(
       event => {
@@ -441,10 +437,10 @@ export const CompileError = () => {
   }
 
   return withContextRoot(
-    <PdfPreviewProvider>
+    <>
       <PdfPreviewPane />
       <Inner />
-    </PdfPreviewProvider>,
+    </>,
     scope
   )
 }
@@ -470,7 +466,7 @@ const compileErrors = [
 
 export const DisplayError = () => {
   return withContextRoot(
-    <PdfPreviewProvider>
+    <>
       {compileErrors.map(error => (
         <div
           key={error}
@@ -480,7 +476,7 @@ export const DisplayError = () => {
           <PdfPreviewError error={error} />
         </div>
       ))}
-    </PdfPreviewProvider>,
+    </>,
     scope
   )
 }
@@ -489,11 +485,9 @@ export const Toolbar = () => {
   useFetchMock(fetchMock => mockCompile(fetchMock, 500))
 
   return withContextRoot(
-    <PdfPreviewProvider>
-      <div className="pdf">
-        <PdfPreviewToolbar />
-      </div>
-    </PdfPreviewProvider>,
+    <div className="pdf">
+      <PdfPreviewToolbar />
+    </div>,
     scope
   )
 }
@@ -505,11 +499,9 @@ export const HybridToolbar = () => {
   })
 
   return withContextRoot(
-    <PdfPreviewProvider>
-      <div className="pdf">
-        <PdfPreviewHybridToolbar />
-      </div>
-    </PdfPreviewProvider>,
+    <div className="pdf">
+      <PdfPreviewHybridToolbar />
+    </div>,
     scope
   )
 }
@@ -530,21 +522,15 @@ export const FileList = () => {
 
 export const Logs = () => {
   useFetchMock(fetchMock => {
-    mockCompile(fetchMock, 0)
+    mockCompileError(fetchMock, 400, 0)
     mockBuildFile(fetchMock)
     mockClearCache(fetchMock)
   })
 
-  useEffect(() => {
-    dispatchProjectJoined()
-  }, [])
-
   return withContextRoot(
-    <PdfPreviewProvider>
-      <div className="pdf">
-        <PdfLogsViewer />
-      </div>
-    </PdfPreviewProvider>,
+    <div className="pdf">
+      <PdfLogsViewer />
+    </div>,
     scope
   )
 }
@@ -577,10 +563,5 @@ export const ValidationIssues = () => {
     dispatchProjectJoined()
   }, [])
 
-  return withContextRoot(
-    <PdfPreviewProvider>
-      <PdfPreviewPane />
-    </PdfPreviewProvider>,
-    scope
-  )
+  return withContextRoot(<PdfPreviewPane />, scope)
 }