diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js
index 7317a364f6..c0d31e6780 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js
+++ b/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 (
{newPreviewToolbar ?
:
}
@@ -21,7 +18,7 @@ function PdfPreviewPane() {
- {showLogs &&
}
+
)
}
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js
index e671b06034..7c828b1ddd 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js
+++ b/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 (
-
-
-
- )
+ return
}
export default withErrorBoundary(memo(PdfPreview), () => (
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js b/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js
index 36f9cca10c..91209b2db8 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js
+++ b/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
diff --git a/services/web/frontend/js/features/pdf-preview/contexts/pdf-preview-context.js b/services/web/frontend/js/features/pdf-preview/contexts/pdf-preview-context.js
deleted file mode 100644
index abd3fc5102..0000000000
--- a/services/web/frontend/js/features/pdf-preview/contexts/pdf-preview-context.js
+++ /dev/null
@@ -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 (
-
- {children}
-
- )
-}
-
-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
-}
diff --git a/services/web/frontend/js/features/pdf-preview/util/compiler.js b/services/web/frontend/js/features/pdf-preview/util/compiler.js
index d29e71a683..1e9f23dd3e 100644
--- a/services/web/frontend/js/features/pdf-preview/util/compiler.js
+++ b/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 = {}) {
diff --git a/services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.js b/services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.js
index 68d59492c8..a1a230d3ff 100644
--- a/services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.js
+++ b/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
}
diff --git a/services/web/frontend/js/shared/context/compile-context.js b/services/web/frontend/js/shared/context/compile-context.js
index 36c694ab3a..11b6fb62fe 100644
--- a/services/web/frontend/js/shared/context/compile-context.js
+++ b/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,
]
)
diff --git a/services/web/frontend/js/shared/context/layout-context.js b/services/web/frontend/js/shared/context/layout-context.js
index 035d4a07f6..57a5162dc6 100644
--- a/services/web/frontend/js/shared/context/layout-context.js
+++ b/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,
]
)
diff --git a/services/web/frontend/js/shared/context/root-context.js b/services/web/frontend/js/shared/context/root-context.js
index 906761bcbe..eb35c981d7 100644
--- a/services/web/frontend/js/shared/context/root-context.js
+++ b/services/web/frontend/js/shared/context/root-context.js
@@ -17,11 +17,11 @@ export function ContextRoot({ children, ide, settings }) {
-
-
+
+
{children}
-
-
+
+
diff --git a/services/web/frontend/stories/pdf-preview.stories.js b/services/web/frontend/stories/pdf-preview.stories.js
index a0725978c1..51633ebc43 100644
--- a/services/web/frontend/stories/pdf-preview.stories.js
+++ b/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(
,
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(
-
+ <>
- ,
+ >,
scope
)
}
@@ -470,7 +466,7 @@ const compileErrors = [
export const DisplayError = () => {
return withContextRoot(
-
+ <>
{compileErrors.map(error => (
))}
- ,
+ >,
scope
)
}
@@ -489,11 +485,9 @@ export const Toolbar = () => {
useFetchMock(fetchMock => mockCompile(fetchMock, 500))
return withContextRoot(
-
-
- ,
+
,
scope
)
}
@@ -505,11 +499,9 @@ export const HybridToolbar = () => {
})
return withContextRoot(
-
-
- ,
+
,
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(
-
-
- ,
+
,
scope
)
}
@@ -577,10 +563,5 @@ export const ValidationIssues = () => {
dispatchProjectJoined()
}, [])
- return withContextRoot(
-
-
- ,
- scope
- )
+ return withContextRoot(
, scope)
}