mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Refactor compile-related code from PDF preview context provider into a separate class (#5341)
GitOrigin-RevId: 96b8bb527fa3d60a5fb84eee2b8f4fabc1726875
This commit is contained in:
parent
515180aeaa
commit
b902bd9265
9 changed files with 403 additions and 386 deletions
|
@ -3,7 +3,7 @@ import Icon from '../../../shared/components/icon'
|
||||||
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
|
import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
|
||||||
import { memo, useCallback } from 'react'
|
import { memo } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
function PdfCompileButton() {
|
function PdfCompileButton() {
|
||||||
|
@ -12,10 +12,10 @@ function PdfCompileButton() {
|
||||||
compiling,
|
compiling,
|
||||||
draft,
|
draft,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
recompile,
|
|
||||||
setAutoCompile,
|
setAutoCompile,
|
||||||
setDraft,
|
setDraft,
|
||||||
setStopOnValidationError,
|
setStopOnValidationError,
|
||||||
|
startCompile,
|
||||||
stopCompile,
|
stopCompile,
|
||||||
stopOnValidationError,
|
stopOnValidationError,
|
||||||
recompileFromScratch,
|
recompileFromScratch,
|
||||||
|
@ -25,10 +25,6 @@ function PdfCompileButton() {
|
||||||
|
|
||||||
const compileButtonLabel = compiling ? t('compiling') + '…' : t('recompile')
|
const compileButtonLabel = compiling ? t('compiling') + '…' : t('recompile')
|
||||||
|
|
||||||
const startCompile = useCallback(() => {
|
|
||||||
recompile()
|
|
||||||
}, [recompile])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledDropdown
|
<ControlledDropdown
|
||||||
className={classnames({
|
className={classnames({
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { memo } from 'react'
|
||||||
export function PdfLogsButtonContent({
|
export function PdfLogsButtonContent({
|
||||||
showLogs,
|
showLogs,
|
||||||
logEntries,
|
logEntries,
|
||||||
autoCompileLintingError,
|
codeCheckFailed,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export function PdfLogsButtonContent({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoCompileLintingError) {
|
if (codeCheckFailed) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon type="exclamation-triangle" />
|
<Icon type="exclamation-triangle" />
|
||||||
|
@ -59,7 +59,7 @@ export function PdfLogsButtonContent({
|
||||||
}
|
}
|
||||||
|
|
||||||
PdfLogsButtonContent.propTypes = {
|
PdfLogsButtonContent.propTypes = {
|
||||||
autoCompileLintingError: PropTypes.bool,
|
codeCheckFailed: PropTypes.bool,
|
||||||
showLogs: PropTypes.bool,
|
showLogs: PropTypes.bool,
|
||||||
logEntries: PropTypes.object,
|
logEntries: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { sendMBOnce } from '../../../infrastructure/event-tracking'
|
||||||
|
|
||||||
function PdfLogsButton() {
|
function PdfLogsButton() {
|
||||||
const {
|
const {
|
||||||
autoCompileLintingError,
|
codeCheckFailed,
|
||||||
stopOnValidationError,
|
|
||||||
error,
|
error,
|
||||||
logEntries,
|
logEntries,
|
||||||
showLogs,
|
showLogs,
|
||||||
|
@ -19,7 +18,7 @@ function PdfLogsButton() {
|
||||||
return 'default'
|
return 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoCompileLintingError && stopOnValidationError) {
|
if (codeCheckFailed) {
|
||||||
return 'danger'
|
return 'danger'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ function PdfLogsButton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'default'
|
return 'default'
|
||||||
}, [autoCompileLintingError, logEntries, showLogs, stopOnValidationError])
|
}, [codeCheckFailed, logEntries, showLogs])
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setShowLogs(value => {
|
setShowLogs(value => {
|
||||||
|
@ -57,9 +56,7 @@ function PdfLogsButton() {
|
||||||
<PdfLogsButtonContent
|
<PdfLogsButtonContent
|
||||||
showLogs={showLogs}
|
showLogs={showLogs}
|
||||||
logEntries={logEntries}
|
logEntries={logEntries}
|
||||||
autoCompileLintingError={
|
codeCheckFailed={codeCheckFailed}
|
||||||
autoCompileLintingError && stopOnValidationError
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,8 +14,7 @@ import ErrorBoundaryFallback from './error-boundary-fallback'
|
||||||
|
|
||||||
function PdfLogsViewer() {
|
function PdfLogsViewer() {
|
||||||
const {
|
const {
|
||||||
autoCompileLintingError,
|
codeCheckFailed,
|
||||||
stopOnValidationError,
|
|
||||||
error,
|
error,
|
||||||
logEntries,
|
logEntries,
|
||||||
rawLog,
|
rawLog,
|
||||||
|
@ -27,7 +26,7 @@ function PdfLogsViewer() {
|
||||||
return (
|
return (
|
||||||
<div className="logs-pane">
|
<div className="logs-pane">
|
||||||
<div className="logs-pane-content">
|
<div className="logs-pane-content">
|
||||||
{autoCompileLintingError && stopOnValidationError && (
|
{codeCheckFailed && (
|
||||||
<div className="log-entry">
|
<div className="log-entry">
|
||||||
<div className="log-entry-header log-entry-header-error">
|
<div className="log-entry-header log-entry-header-error">
|
||||||
<div className="log-entry-header-icon-container">
|
<div className="log-entry-header-icon-container">
|
||||||
|
|
|
@ -9,32 +9,20 @@ import {
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import useScopeValue from '../../../shared/context/util/scope-value-hook'
|
import useScopeValue from '../../../shared/context/util/scope-value-hook'
|
||||||
import { useProjectContext } from '../../../shared/context/project-context'
|
import { useProjectContext } from '../../../shared/context/project-context'
|
||||||
import getMeta from '../../../utils/meta'
|
|
||||||
import { deleteJSON, postJSON } from '../../../infrastructure/fetch-json'
|
|
||||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||||
import {
|
import {
|
||||||
buildLogEntryAnnotations,
|
buildLogEntryAnnotations,
|
||||||
handleOutputFiles,
|
handleOutputFiles,
|
||||||
} from '../util/output-files'
|
} from '../util/output-files'
|
||||||
import { debounce } from 'lodash'
|
|
||||||
import { useIdeContext } from '../../../shared/context/ide-context'
|
|
||||||
import {
|
import {
|
||||||
send,
|
send,
|
||||||
sendMB,
|
sendMB,
|
||||||
sendMBSampled,
|
sendMBSampled,
|
||||||
} from '../../../infrastructure/event-tracking'
|
} from '../../../infrastructure/event-tracking'
|
||||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||||
import { isMainFile } from '../util/editor-files'
|
|
||||||
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
||||||
|
import DocumentCompiler from '../util/compiler'
|
||||||
const AUTO_COMPILE_MAX_WAIT = 5000
|
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||||
// We add a 1 second debounce to sending user changes to server if they aren't
|
|
||||||
// collaborating with anyone. This needs to be higher than that, and allow for
|
|
||||||
// client to server latency, otherwise we compile before the op reaches the server
|
|
||||||
// and then again on ack.
|
|
||||||
const AUTO_COMPILE_DEBOUNCE = 2000
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(window.location.search)
|
|
||||||
|
|
||||||
export const PdfPreviewContext = createContext(undefined)
|
export const PdfPreviewContext = createContext(undefined)
|
||||||
|
|
||||||
|
@ -45,7 +33,9 @@ PdfPreviewProvider.propTypes = {
|
||||||
export default function PdfPreviewProvider({ children }) {
|
export default function PdfPreviewProvider({ children }) {
|
||||||
const ide = useIdeContext()
|
const ide = useIdeContext()
|
||||||
|
|
||||||
const { _id: projectId, rootDoc_id: rootDocId } = useProjectContext()
|
const project = useProjectContext()
|
||||||
|
|
||||||
|
const projectId = project._id
|
||||||
|
|
||||||
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
|
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
|
||||||
|
|
||||||
|
@ -65,10 +55,7 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
const [, setLogEntryAnnotations] = useScopeValue('pdf.logEntryAnnotations')
|
const [, setLogEntryAnnotations] = useScopeValue('pdf.logEntryAnnotations')
|
||||||
|
|
||||||
// the id of the CLSI server which ran the compile
|
// the id of the CLSI server which ran the compile
|
||||||
const [clsiServerId, setClsiServerId] = useScopeValue('ide.clsiServerId')
|
const [, setClsiServerId] = useScopeValue('ide.clsiServerId')
|
||||||
|
|
||||||
// the compile group (standard or priority)
|
|
||||||
const [compileGroup, setCompileGroup] = useScopeValue('ide.compileGroup')
|
|
||||||
|
|
||||||
// whether to display the editor and preview side-by-side or full-width ("flat")
|
// whether to display the editor and preview side-by-side or full-width ("flat")
|
||||||
const [pdfLayout, setPdfLayout] = useScopeValue('ui.pdfLayout')
|
const [pdfLayout, setPdfLayout] = useScopeValue('ui.pdfLayout')
|
||||||
|
@ -79,6 +66,9 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
// whether a compile is in progress
|
// whether a compile is in progress
|
||||||
const [compiling, setCompiling] = useState(false)
|
const [compiling, setCompiling] = useState(false)
|
||||||
|
|
||||||
|
// data received in response to a compile request
|
||||||
|
const [data, setData] = useState()
|
||||||
|
|
||||||
// whether the project has been compiled yet
|
// whether the project has been compiled yet
|
||||||
const [compiledOnce, setCompiledOnce] = useState(false)
|
const [compiledOnce, setCompiledOnce] = useState(false)
|
||||||
|
|
||||||
|
@ -117,10 +107,7 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
// the id of the currently open document in the editor
|
// the Document currently open in the editor
|
||||||
const [currentDocId] = useScopeValue('editor.open_doc_id')
|
|
||||||
|
|
||||||
// the Document currently open in the editor?
|
|
||||||
const [currentDoc] = useScopeValue('editor.sharejs_doc')
|
const [currentDoc] = useScopeValue('editor.sharejs_doc')
|
||||||
|
|
||||||
// whether the PDF view is hidden
|
// whether the PDF view is hidden
|
||||||
|
@ -137,6 +124,35 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
|
|
||||||
const { signal } = useAbortController()
|
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
|
// pass the "uncompiled" value up into the scope for use outside this context provider
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUncompiled(changedAt > 0)
|
setUncompiled(changedAt > 0)
|
||||||
|
@ -151,46 +167,26 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
[_setAutoCompile]
|
[_setAutoCompile]
|
||||||
)
|
)
|
||||||
|
|
||||||
// parse the text of the current doc in the editor
|
// always compile the PDF once after opening the project, after the doc has loaded
|
||||||
// if it contains "\documentclass" then use this as the root doc
|
useEffect(() => {
|
||||||
const getRootDocOverrideId = useCallback(() => {
|
if (!compiledOnce && currentDoc) {
|
||||||
if (currentDocId === rootDocId) {
|
setCompiledOnce(true)
|
||||||
return null // no need to override when in the root doc itself
|
compiler.compile({ isAutoCompileOnLoad: true })
|
||||||
}
|
}
|
||||||
|
}, [compiledOnce, currentDoc, compiler])
|
||||||
if (currentDoc) {
|
|
||||||
const doc = currentDoc.getSnapshot()
|
|
||||||
|
|
||||||
if (doc) {
|
|
||||||
return isMainFile(doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}, [currentDoc, currentDocId, rootDocId])
|
|
||||||
|
|
||||||
// TODO: remove this?
|
|
||||||
const sendCompileMetrics = useCallback(() => {
|
|
||||||
if (compiledOnce && !error && !window.user.alphaProgram) {
|
|
||||||
const metadata = {
|
|
||||||
errors: logEntries.errors.length,
|
|
||||||
warnings: logEntries.warnings.length,
|
|
||||||
typesetting: logEntries.typesetting.length,
|
|
||||||
newPdfPreview: true,
|
|
||||||
}
|
|
||||||
sendMBSampled('compile-result', metadata, 0.01)
|
|
||||||
}
|
|
||||||
}, [compiledOnce, error, logEntries])
|
|
||||||
|
|
||||||
// handle the data returned from a compile request
|
// handle the data returned from a compile request
|
||||||
const handleCompileData = useCallback(
|
// note: this should _only_ run when `data` changes,
|
||||||
(data, options) => {
|
// the other dependencies must all be static
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
if (data.clsiServerId) {
|
if (data.clsiServerId) {
|
||||||
setClsiServerId(data.clsiServerId)
|
setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController
|
||||||
|
compiler.clsiServerId = data.clsiServerId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.compileGroup) {
|
if (data.compileGroup) {
|
||||||
setCompileGroup(data.compileGroup)
|
compiler.compileGroup = data.compileGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.outputFiles) {
|
if (data.outputFiles) {
|
||||||
|
@ -203,13 +199,27 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
setPdfDownloadUrl(result.pdfDownloadUrl)
|
setPdfDownloadUrl(result.pdfDownloadUrl)
|
||||||
setPdfUrl(result.pdfUrl)
|
setPdfUrl(result.pdfUrl)
|
||||||
setRawLog(result.log)
|
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) {
|
switch (data.status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setShowLogs(false) // TODO: always?
|
setShowLogs(false)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'clsi-maintenance':
|
case 'clsi-maintenance':
|
||||||
|
@ -238,7 +248,7 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'autocompile-backoff':
|
case 'autocompile-backoff':
|
||||||
if (!options.isAutoCompileOnLoad) {
|
if (!data.options.isAutoCompileOnLoad) {
|
||||||
setError('autocompile-disabled')
|
setError('autocompile-disabled')
|
||||||
setAutoCompile(false)
|
setAutoCompile(false)
|
||||||
sendMB('autocompile-rate-limited', { hasPremiumCompile })
|
sendMB('autocompile-rate-limited', { hasPremiumCompile })
|
||||||
|
@ -258,97 +268,21 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
setError('error')
|
setError('error')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[
|
}, [
|
||||||
hasPremiumCompile,
|
compiler,
|
||||||
ide.fileTreeManager,
|
data,
|
||||||
isProjectOwner,
|
ide,
|
||||||
projectId,
|
hasPremiumCompile,
|
||||||
setAutoCompile,
|
isProjectOwner,
|
||||||
setClsiServerId,
|
projectId,
|
||||||
setCompileGroup,
|
setAutoCompile,
|
||||||
setLogEntries,
|
setClsiServerId,
|
||||||
setLogEntryAnnotations,
|
setLogEntries,
|
||||||
setPdfDownloadUrl,
|
setLogEntryAnnotations,
|
||||||
setPdfUrl,
|
setPdfDownloadUrl,
|
||||||
]
|
setPdfUrl,
|
||||||
)
|
])
|
||||||
|
|
||||||
const buildCompileParams = useCallback(
|
|
||||||
options => {
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
|
|
||||||
if (clsiServerId) {
|
|
||||||
params.set('clsiserverid', clsiServerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.isAutoCompileOnLoad || options.isAutoCompileOnChange) {
|
|
||||||
params.set('auto_compile', 'true')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getMeta('ol-enablePdfCaching')) {
|
|
||||||
params.set('enable_pdf_caching', 'true')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchParams.get('file_line_errors') === 'true') {
|
|
||||||
params.file_line_errors = 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
},
|
|
||||||
[clsiServerId]
|
|
||||||
)
|
|
||||||
|
|
||||||
// run a compile
|
|
||||||
const recompile = useCallback(
|
|
||||||
(options = {}) => {
|
|
||||||
if (compiling) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMBSampled('editor-recompile-sampled', options)
|
|
||||||
|
|
||||||
setChangedAt(0) // NOTE: this sets uncompiled to false
|
|
||||||
setCompiling(true)
|
|
||||||
setValidationIssues(undefined)
|
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('flush-changes')) // TODO: wait for this?
|
|
||||||
|
|
||||||
postJSON(`/project/${projectId}/compile?${buildCompileParams(options)}`, {
|
|
||||||
body: {
|
|
||||||
rootDoc_id: getRootDocOverrideId(),
|
|
||||||
draft,
|
|
||||||
check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused
|
|
||||||
// use incremental compile for all users but revert to a full compile
|
|
||||||
// if there was previously a server error
|
|
||||||
incrementalCompilesEnabled: !error,
|
|
||||||
},
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
handleCompileData(data, options)
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// console.error(error)
|
|
||||||
setError(error.info?.statusCode === 429 ? 'rate-limited' : 'error')
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setCompiling(false)
|
|
||||||
sendCompileMetrics()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[
|
|
||||||
compiling,
|
|
||||||
projectId,
|
|
||||||
buildCompileParams,
|
|
||||||
getRootDocOverrideId,
|
|
||||||
draft,
|
|
||||||
error,
|
|
||||||
handleCompileData,
|
|
||||||
sendCompileMetrics,
|
|
||||||
signal,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
// switch to logs if there's an error
|
// switch to logs if there's an error
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -360,7 +294,7 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
// recompile on key press
|
// recompile on key press
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = event => {
|
const listener = event => {
|
||||||
recompile(event.detail)
|
compiler.compile(event.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('pdf:recompile', listener)
|
window.addEventListener('pdf:recompile', listener)
|
||||||
|
@ -368,73 +302,40 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('pdf:recompile', listener)
|
window.removeEventListener('pdf:recompile', listener)
|
||||||
}
|
}
|
||||||
}, [recompile])
|
}, [compiler])
|
||||||
|
|
||||||
// always compile the PDF once, when joining the project
|
|
||||||
useEffect(() => {
|
|
||||||
const listener = () => {
|
|
||||||
if (!compiledOnce) {
|
|
||||||
setCompiledOnce(true)
|
|
||||||
recompile({ isAutoCompileOnLoad: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('project:joined', listener)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('project:joined', listener)
|
|
||||||
}
|
|
||||||
}, [compiledOnce, recompile])
|
|
||||||
|
|
||||||
// whether there has been an autocompile linting error, if syntax validation is switched on
|
// whether there has been an autocompile linting error, if syntax validation is switched on
|
||||||
const autoCompileLintingError = Boolean(
|
const autoCompileLintingError = Boolean(
|
||||||
autoCompile && syntaxValidation && hasLintingError
|
autoCompile && syntaxValidation && hasLintingError
|
||||||
)
|
)
|
||||||
|
|
||||||
// the project has visible changes
|
const codeCheckFailed = stopOnValidationError && autoCompileLintingError
|
||||||
|
|
||||||
|
// show that the project has pending changes
|
||||||
const hasChanges = Boolean(
|
const hasChanges = Boolean(
|
||||||
autoCompile &&
|
autoCompile && uncompiled && compiledOnce && !codeCheckFailed
|
||||||
uncompiled &&
|
|
||||||
compiledOnce &&
|
|
||||||
!(stopOnValidationError && autoCompileLintingError)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// the project is available for auto-compiling
|
// the project is available for auto-compiling
|
||||||
const canAutoCompile = Boolean(
|
const canAutoCompile = Boolean(
|
||||||
autoCompile &&
|
autoCompile && !compiling && !pdfHidden && !codeCheckFailed
|
||||||
!compiling &&
|
|
||||||
!pdfHidden &&
|
|
||||||
!(stopOnValidationError && autoCompileLintingError)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// a debounced wrapper around the recompile function, used for auto-compile
|
// call the debounced autocompile function if the project is available for auto-compiling and it has changed
|
||||||
const [debouncedAutoCompile] = useState(() => {
|
|
||||||
return debounce(
|
|
||||||
() => {
|
|
||||||
recompile({ isAutoCompileOnChange: true })
|
|
||||||
},
|
|
||||||
AUTO_COMPILE_DEBOUNCE,
|
|
||||||
{
|
|
||||||
maxWait: AUTO_COMPILE_MAX_WAIT,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// call the debounced recompile function if the project is available for auto-compiling and it has changed
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canAutoCompile && changedAt > 0) {
|
if (canAutoCompile && changedAt > 0) {
|
||||||
debouncedAutoCompile()
|
compiler.debouncedAutoCompile()
|
||||||
} else {
|
} else {
|
||||||
debouncedAutoCompile.cancel()
|
compiler.debouncedAutoCompile.cancel()
|
||||||
}
|
}
|
||||||
}, [canAutoCompile, debouncedAutoCompile, recompile, changedAt])
|
}, [compiler, canAutoCompile, changedAt])
|
||||||
|
|
||||||
// cancel debounced recompile on unmount
|
// cancel debounced recompile on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
debouncedAutoCompile.cancel()
|
compiler.debouncedAutoCompile.cancel()
|
||||||
}
|
}
|
||||||
}, [debouncedAutoCompile])
|
}, [compiler])
|
||||||
|
|
||||||
// record doc changes when notified by the editor
|
// record doc changes when notified by the editor
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -451,52 +352,31 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// send a request to stop the current compile
|
// start a compile manually
|
||||||
|
const startCompile = useCallback(() => {
|
||||||
|
compiler.compile()
|
||||||
|
}, [compiler])
|
||||||
|
|
||||||
|
// stop a compile manually
|
||||||
const stopCompile = useCallback(() => {
|
const stopCompile = useCallback(() => {
|
||||||
// TODO: stoppingCompile state?
|
compiler.stopCompile()
|
||||||
|
}, [compiler])
|
||||||
const params = new URLSearchParams()
|
|
||||||
|
|
||||||
if (clsiServerId) {
|
|
||||||
params.set('clsiserverid', clsiServerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return postJSON(`/project/${projectId}/compile/stop?${params}`, { signal })
|
|
||||||
.catch(error => {
|
|
||||||
setError(error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setCompiling(false)
|
|
||||||
})
|
|
||||||
}, [projectId, clsiServerId, signal])
|
|
||||||
|
|
||||||
|
// clear the compile cache
|
||||||
const clearCache = useCallback(() => {
|
const clearCache = useCallback(() => {
|
||||||
setClearingCache(true)
|
setClearingCache(true)
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
return compiler.clearCache().finally(() => {
|
||||||
|
setClearingCache(false)
|
||||||
if (clsiServerId) {
|
})
|
||||||
params.set('clsiserverid', clsiServerId)
|
}, [compiler])
|
||||||
}
|
|
||||||
|
|
||||||
return deleteJSON(`/project/${projectId}/output?${params}`, { signal })
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error)
|
|
||||||
setError('clear-cache')
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setClearingCache(false)
|
|
||||||
})
|
|
||||||
}, [clsiServerId, projectId, setError, signal])
|
|
||||||
|
|
||||||
// clear the cache then run a compile, triggered by a menu item
|
// clear the cache then run a compile, triggered by a menu item
|
||||||
const recompileFromScratch = useCallback(() => {
|
const recompileFromScratch = useCallback(() => {
|
||||||
setClearingCache(true)
|
|
||||||
clearCache().then(() => {
|
clearCache().then(() => {
|
||||||
setClearingCache(false)
|
compiler.compile()
|
||||||
recompile()
|
|
||||||
})
|
})
|
||||||
}, [clearCache, recompile])
|
}, [clearCache, compiler])
|
||||||
|
|
||||||
// switch to either side-by-side or flat (full-width) layout
|
// switch to either side-by-side or flat (full-width) layout
|
||||||
const switchLayout = useCallback(() => {
|
const switchLayout = useCallback(() => {
|
||||||
|
@ -512,12 +392,9 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
autoCompile,
|
autoCompile,
|
||||||
autoCompileLintingError,
|
codeCheckFailed,
|
||||||
clearCache,
|
clearCache,
|
||||||
clearingCache,
|
clearingCache,
|
||||||
clsiServerId,
|
|
||||||
compileGroup,
|
|
||||||
compiledOnce,
|
|
||||||
compiling,
|
compiling,
|
||||||
draft,
|
draft,
|
||||||
error,
|
error,
|
||||||
|
@ -529,23 +406,14 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
pdfLayout,
|
pdfLayout,
|
||||||
pdfUrl,
|
pdfUrl,
|
||||||
rawLog,
|
rawLog,
|
||||||
recompile,
|
|
||||||
recompileFromScratch,
|
recompileFromScratch,
|
||||||
setAutoCompile,
|
setAutoCompile,
|
||||||
setClsiServerId,
|
|
||||||
setCompileGroup,
|
|
||||||
setCompiledOnce,
|
|
||||||
setDraft,
|
setDraft,
|
||||||
setError,
|
setHasLintingError, // only for stories
|
||||||
setHasLintingError, // for story
|
|
||||||
setLogEntries,
|
|
||||||
setPdfDownloadUrl,
|
|
||||||
setPdfLayout,
|
|
||||||
setPdfUrl,
|
|
||||||
setShowLogs,
|
setShowLogs,
|
||||||
setStopOnValidationError,
|
setStopOnValidationError,
|
||||||
setUiView,
|
|
||||||
showLogs,
|
showLogs,
|
||||||
|
startCompile,
|
||||||
stopCompile,
|
stopCompile,
|
||||||
stopOnValidationError,
|
stopOnValidationError,
|
||||||
switchLayout,
|
switchLayout,
|
||||||
|
@ -554,12 +422,9 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
autoCompile,
|
autoCompile,
|
||||||
autoCompileLintingError,
|
codeCheckFailed,
|
||||||
clearCache,
|
clearCache,
|
||||||
clearingCache,
|
clearingCache,
|
||||||
clsiServerId,
|
|
||||||
compileGroup,
|
|
||||||
compiledOnce,
|
|
||||||
compiling,
|
compiling,
|
||||||
draft,
|
draft,
|
||||||
error,
|
error,
|
||||||
|
@ -571,22 +436,13 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
pdfLayout,
|
pdfLayout,
|
||||||
pdfUrl,
|
pdfUrl,
|
||||||
rawLog,
|
rawLog,
|
||||||
recompile,
|
|
||||||
recompileFromScratch,
|
recompileFromScratch,
|
||||||
setAutoCompile,
|
setAutoCompile,
|
||||||
setClsiServerId,
|
|
||||||
setCompileGroup,
|
|
||||||
setCompiledOnce,
|
|
||||||
setDraft,
|
setDraft,
|
||||||
setError,
|
setHasLintingError, // only for stories
|
||||||
setHasLintingError,
|
|
||||||
setLogEntries,
|
|
||||||
setPdfDownloadUrl,
|
|
||||||
setPdfLayout,
|
|
||||||
setPdfUrl,
|
|
||||||
setStopOnValidationError,
|
setStopOnValidationError,
|
||||||
setUiView,
|
|
||||||
showLogs,
|
showLogs,
|
||||||
|
startCompile,
|
||||||
stopCompile,
|
stopCompile,
|
||||||
stopOnValidationError,
|
stopOnValidationError,
|
||||||
switchLayout,
|
switchLayout,
|
||||||
|
@ -604,12 +460,9 @@ export default function PdfPreviewProvider({ children }) {
|
||||||
PdfPreviewContext.Provider.propTypes = {
|
PdfPreviewContext.Provider.propTypes = {
|
||||||
value: PropTypes.shape({
|
value: PropTypes.shape({
|
||||||
autoCompile: PropTypes.bool.isRequired,
|
autoCompile: PropTypes.bool.isRequired,
|
||||||
autoCompileLintingError: PropTypes.bool.isRequired,
|
|
||||||
clearCache: PropTypes.func.isRequired,
|
clearCache: PropTypes.func.isRequired,
|
||||||
clearingCache: PropTypes.bool.isRequired,
|
clearingCache: PropTypes.bool.isRequired,
|
||||||
clsiServerId: PropTypes.string,
|
codeCheckFailed: PropTypes.bool.isRequired,
|
||||||
compileGroup: PropTypes.string,
|
|
||||||
compiledOnce: PropTypes.bool.isRequired,
|
|
||||||
compiling: PropTypes.bool.isRequired,
|
compiling: PropTypes.bool.isRequired,
|
||||||
draft: PropTypes.bool.isRequired,
|
draft: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
|
@ -621,23 +474,14 @@ PdfPreviewContext.Provider.propTypes = {
|
||||||
pdfLayout: PropTypes.string,
|
pdfLayout: PropTypes.string,
|
||||||
pdfUrl: PropTypes.string,
|
pdfUrl: PropTypes.string,
|
||||||
rawLog: PropTypes.string,
|
rawLog: PropTypes.string,
|
||||||
recompile: PropTypes.func.isRequired,
|
|
||||||
recompileFromScratch: PropTypes.func.isRequired,
|
recompileFromScratch: PropTypes.func.isRequired,
|
||||||
setAutoCompile: PropTypes.func.isRequired,
|
setAutoCompile: PropTypes.func.isRequired,
|
||||||
setClsiServerId: PropTypes.func.isRequired,
|
|
||||||
setCompileGroup: PropTypes.func.isRequired,
|
|
||||||
setCompiledOnce: PropTypes.func.isRequired,
|
|
||||||
setDraft: PropTypes.func.isRequired,
|
setDraft: PropTypes.func.isRequired,
|
||||||
setError: PropTypes.func.isRequired,
|
|
||||||
setHasLintingError: PropTypes.func.isRequired, // only for storybook
|
setHasLintingError: PropTypes.func.isRequired, // only for storybook
|
||||||
setLogEntries: PropTypes.func.isRequired,
|
|
||||||
setPdfDownloadUrl: PropTypes.func.isRequired,
|
|
||||||
setPdfLayout: PropTypes.func.isRequired,
|
|
||||||
setPdfUrl: PropTypes.func.isRequired,
|
|
||||||
setShowLogs: PropTypes.func.isRequired,
|
setShowLogs: PropTypes.func.isRequired,
|
||||||
setStopOnValidationError: PropTypes.func.isRequired,
|
setStopOnValidationError: PropTypes.func.isRequired,
|
||||||
setUiView: PropTypes.func.isRequired,
|
|
||||||
showLogs: PropTypes.bool.isRequired,
|
showLogs: PropTypes.bool.isRequired,
|
||||||
|
startCompile: PropTypes.func.isRequired,
|
||||||
stopCompile: PropTypes.func.isRequired,
|
stopCompile: PropTypes.func.isRequired,
|
||||||
stopOnValidationError: PropTypes.bool.isRequired,
|
stopOnValidationError: PropTypes.bool.isRequired,
|
||||||
switchLayout: PropTypes.func.isRequired,
|
switchLayout: PropTypes.func.isRequired,
|
||||||
|
|
179
services/web/frontend/js/features/pdf-preview/util/compiler.js
Normal file
179
services/web/frontend/js/features/pdf-preview/util/compiler.js
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
import { isMainFile } from './editor-files'
|
||||||
|
import getMeta from '../../../utils/meta'
|
||||||
|
import { sendMBSampled } from '../../../infrastructure/event-tracking'
|
||||||
|
import { deleteJSON, postJSON } from '../../../infrastructure/fetch-json'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
|
||||||
|
const AUTO_COMPILE_MAX_WAIT = 5000
|
||||||
|
// We add a 1 second debounce to sending user changes to server if they aren't
|
||||||
|
// collaborating with anyone. This needs to be higher than that, and allow for
|
||||||
|
// client to server latency, otherwise we compile before the op reaches the server
|
||||||
|
// and then again on ack.
|
||||||
|
const AUTO_COMPILE_DEBOUNCE = 2000
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
export default class DocumentCompiler {
|
||||||
|
constructor({
|
||||||
|
project,
|
||||||
|
setChangedAt,
|
||||||
|
setCompiling,
|
||||||
|
setData,
|
||||||
|
setError,
|
||||||
|
signal,
|
||||||
|
}) {
|
||||||
|
this.project = project
|
||||||
|
this.setChangedAt = setChangedAt
|
||||||
|
this.setCompiling = setCompiling
|
||||||
|
this.setData = setData
|
||||||
|
this.setError = setError
|
||||||
|
this.signal = signal
|
||||||
|
|
||||||
|
this.clsiServerId = null
|
||||||
|
this.currentDoc = null
|
||||||
|
this.error = undefined
|
||||||
|
this.timer = 0
|
||||||
|
|
||||||
|
this.debouncedAutoCompile = debounce(
|
||||||
|
() => {
|
||||||
|
this.compile({ isAutoCompileOnChange: true })
|
||||||
|
},
|
||||||
|
AUTO_COMPILE_DEBOUNCE,
|
||||||
|
{
|
||||||
|
maxWait: AUTO_COMPILE_MAX_WAIT,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.signal.abort()
|
||||||
|
this.debouncedAutoCompile.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main "compile" function.
|
||||||
|
// Call this directly to run a compile now, otherwise call debouncedAutoCompile.
|
||||||
|
async compile(options = {}) {
|
||||||
|
// set "compiling" to true (in the React component's state), and return if it was already true
|
||||||
|
let wasCompiling
|
||||||
|
|
||||||
|
this.setCompiling(oldValue => {
|
||||||
|
wasCompiling = oldValue
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (wasCompiling) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// log a sample of the compile requests
|
||||||
|
sendMBSampled('editor-recompile-sampled', options)
|
||||||
|
|
||||||
|
// reset values
|
||||||
|
this.setChangedAt(0)
|
||||||
|
this.validationIssues = undefined
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('flush-changes')) // TODO: wait for this?
|
||||||
|
|
||||||
|
const params = this.buildQueryParams()
|
||||||
|
this.addCompileParams(params, options)
|
||||||
|
|
||||||
|
const data = await postJSON(
|
||||||
|
`/project/${this.project._id}/compile?${params}`,
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
rootDoc_id: this.getRootDocOverrideId(),
|
||||||
|
draft: this.draft,
|
||||||
|
check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused
|
||||||
|
// use incremental compile for all users but revert to a full compile
|
||||||
|
// if there was previously a server error
|
||||||
|
incrementalCompilesEnabled: !this.error,
|
||||||
|
},
|
||||||
|
signal: this.signal,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data.options = options
|
||||||
|
this.setData(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.setError(error.info?.statusCode === 429 ? 'rate-limited' : 'error')
|
||||||
|
} finally {
|
||||||
|
this.setCompiling(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the text of the current doc in the editor
|
||||||
|
// if it contains "\documentclass" then use this as the root doc
|
||||||
|
getRootDocOverrideId() {
|
||||||
|
// only override when not in the root doc itself
|
||||||
|
if (this.currentDoc.doc_id !== this.project.rootDoc_id) {
|
||||||
|
const snapshot = this.currentDoc.getSnapshot()
|
||||||
|
|
||||||
|
if (snapshot && isMainFile(snapshot)) {
|
||||||
|
return this.currentDoc.doc_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the query parameters added to all requests
|
||||||
|
buildQueryParams() {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
// the id of the CLSI server that processed the previous compile request
|
||||||
|
if (this.clsiServerId) {
|
||||||
|
params.set('clsiserverid', this.clsiServerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// add extra query parameters to the compile request
|
||||||
|
addCompileParams(params, options) {
|
||||||
|
// tell the server whether this is an automatic or manual compile request
|
||||||
|
if (options.isAutoCompileOnLoad || options.isAutoCompileOnChange) {
|
||||||
|
params.set('auto_compile', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the feature flag to enable PDF caching in a ServiceWorker
|
||||||
|
if (getMeta('ol-enablePdfCaching')) {
|
||||||
|
params.set('enable_pdf_caching', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the feature flag to enable "file line errors"
|
||||||
|
if (searchParams.get('file_line_errors') === 'true') {
|
||||||
|
params.file_line_errors = 'true'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a request to stop the current compile
|
||||||
|
stopCompile() {
|
||||||
|
// NOTE: no stoppingCompile state, as this should happen fairly quickly
|
||||||
|
// and doesn't matter if it runs twice.
|
||||||
|
|
||||||
|
const params = this.buildQueryParams()
|
||||||
|
|
||||||
|
return postJSON(`/project/${this.project._id}/compile/stop?${params}`, {
|
||||||
|
signal: this.signal,
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
this.setError('error')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setCompiling(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache() {
|
||||||
|
const params = this.buildQueryParams()
|
||||||
|
|
||||||
|
return deleteJSON(`/project/${this.project._id}/output?${params}`, {
|
||||||
|
signal: this.signal,
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
this.setError('clear-cache')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,6 +109,6 @@ export const Interactive = () => {
|
||||||
<PdfJsViewer url="/build/output.pdf" />
|
<PdfJsViewer url="/build/output.pdf" />
|
||||||
<Inner />
|
<Inner />
|
||||||
</div>,
|
</div>,
|
||||||
{ scope: { project } }
|
{ project }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,12 @@ const scope = {
|
||||||
},
|
},
|
||||||
hasLintingError: false,
|
hasLintingError: false,
|
||||||
$applyAsync: () => {},
|
$applyAsync: () => {},
|
||||||
|
editor: {
|
||||||
|
sharejs_doc: {
|
||||||
|
doc_id: 'test-doc',
|
||||||
|
getSnapshot: () => 'some doc content',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatchProjectJoined = () => {
|
const dispatchProjectJoined = () => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ const outputFiles = [
|
||||||
{
|
{
|
||||||
path: 'output.pdf',
|
path: 'output.pdf',
|
||||||
build: '123',
|
build: '123',
|
||||||
// url: 'about:blank', // TODO: PDF URL to render
|
url: '/build/output.pdf', // TODO: PDF URL to render
|
||||||
type: 'pdf',
|
type: 'pdf',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -119,6 +119,18 @@ const storeAndFireEvent = (key, value) => {
|
||||||
fireEvent(window, new StorageEvent('storage', { key }))
|
fireEvent(window, new StorageEvent('storage', { key }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scope = {
|
||||||
|
settings: {
|
||||||
|
syntaxValidation: false,
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
sharejs_doc: {
|
||||||
|
doc_id: 'test-doc',
|
||||||
|
getSnapshot: () => 'some doc content',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
describe('<PdfPreview/>', function () {
|
describe('<PdfPreview/>', function () {
|
||||||
var clock
|
var clock
|
||||||
|
|
||||||
|
@ -138,50 +150,31 @@ describe('<PdfPreview/>', function () {
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the PDF preview', function () {
|
it('renders the PDF preview', async function () {
|
||||||
renderWithEditorContext(<PdfPreview />)
|
|
||||||
|
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('runs a compile only on the first project:joined event', async function () {
|
|
||||||
mockCompile()
|
mockCompile()
|
||||||
mockBuildFile()
|
mockBuildFile()
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
// wait for "compile on load" to finish
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
// fire another project:joined event => no compile
|
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
|
||||||
|
|
||||||
expect(fetchMock.calls()).to.have.length(3)
|
|
||||||
|
|
||||||
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
|
|
||||||
expect(fetchMock.called('express:/build/:file')).to.be.true // TODO: actual path
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('runs a compile when the Recompile button is pressed', async function () {
|
it('runs a compile when the Recompile button is pressed', async function () {
|
||||||
mockCompile()
|
mockCompile()
|
||||||
mockBuildFile()
|
mockBuildFile()
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
// wait for "compile on load" to finish
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
// press the Recompile button => compile
|
// press the Recompile button => compile
|
||||||
const button = screen.getByRole('button', { name: 'Recompile' })
|
const button = screen.getByRole('button', { name: 'Recompile' })
|
||||||
button.click()
|
button.click()
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
expect(fetchMock.calls()).to.have.length(6)
|
expect(fetchMock.calls()).to.have.length(6)
|
||||||
|
@ -191,12 +184,10 @@ describe('<PdfPreview/>', function () {
|
||||||
mockCompile()
|
mockCompile()
|
||||||
mockBuildFile()
|
mockBuildFile()
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
// wait for "compile on load" to finish
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
// switch on auto compile
|
// switch on auto compile
|
||||||
|
@ -206,7 +197,7 @@ describe('<PdfPreview/>', function () {
|
||||||
fireEvent(window, new CustomEvent('doc:changed'))
|
fireEvent(window, new CustomEvent('doc:changed'))
|
||||||
clock.tick(2000) // AUTO_COMPILE_DEBOUNCE
|
clock.tick(2000) // AUTO_COMPILE_DEBOUNCE
|
||||||
|
|
||||||
await screen.findByText('Compiling…')
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
expect(fetchMock.calls()).to.have.length(6)
|
expect(fetchMock.calls()).to.have.length(6)
|
||||||
|
@ -216,17 +207,14 @@ describe('<PdfPreview/>', function () {
|
||||||
mockCompile()
|
mockCompile()
|
||||||
mockBuildFile()
|
mockBuildFile()
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
// wait for "compile on load" to finish
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
// make sure auto compile is switched off
|
// make sure auto compile is switched off
|
||||||
storeAndFireEvent('autocompile_enabled:project123', false)
|
storeAndFireEvent('autocompile_enabled:project123', false)
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
|
||||||
|
|
||||||
// fire a doc:changed event => no compile
|
// fire a doc:changed event => no compile
|
||||||
fireEvent(window, new CustomEvent('doc:changed'))
|
fireEvent(window, new CustomEvent('doc:changed'))
|
||||||
|
@ -242,21 +230,19 @@ describe('<PdfPreview/>', function () {
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />, {
|
renderWithEditorContext(<PdfPreview />, {
|
||||||
scope: {
|
scope: {
|
||||||
|
...scope,
|
||||||
'settings.syntaxValidation': true, // enable linting in the editor
|
'settings.syntaxValidation': true, // enable linting in the editor
|
||||||
hasLintingError: true, // mock a linting error
|
hasLintingError: true, // mock a linting error
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
// wait for "compile on load" to finish
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
// switch on auto compile and syntax checking
|
// switch on auto compile and syntax checking
|
||||||
storeAndFireEvent('autocompile_enabled:project123', true)
|
storeAndFireEvent('autocompile_enabled:project123', true)
|
||||||
storeAndFireEvent('stop_on_validation_error:project123', true)
|
storeAndFireEvent('stop_on_validation_error:project123', true)
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
|
||||||
|
|
||||||
// fire a doc:changed event => no compile
|
// fire a doc:changed event => no compile
|
||||||
fireEvent(window, new CustomEvent('doc:changed'))
|
fireEvent(window, new CustomEvent('doc:changed'))
|
||||||
|
@ -270,13 +256,12 @@ describe('<PdfPreview/>', function () {
|
||||||
it('displays an error message if there was a compile error', async function () {
|
it('displays an error message if there was a compile error', async function () {
|
||||||
mockCompileError('compile-in-progress')
|
mockCompileError('compile-in-progress')
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
// wait for "compile on load" to finish
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
'Please wait for your other compile to finish before trying again.'
|
'Please wait for your other compile to finish before trying again.'
|
||||||
)
|
)
|
||||||
|
@ -285,27 +270,6 @@ describe('<PdfPreview/>', function () {
|
||||||
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
|
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables the view logs button if there is a compile error', async function () {
|
|
||||||
mockCompileError()
|
|
||||||
mockBuildFile()
|
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
|
||||||
|
|
||||||
const logsButton = screen.getByRole('button', { name: 'View logs' })
|
|
||||||
expect(logsButton.hasAttribute('disabled')).to.be.false
|
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
expect(logsButton.hasAttribute('disabled')).to.be.false
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
|
||||||
expect(logsButton.hasAttribute('disabled')).to.be.true
|
|
||||||
|
|
||||||
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
|
|
||||||
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays error messages if there were validation problems', async function () {
|
it('displays error messages if there were validation problems', async function () {
|
||||||
const validationProblems = {
|
const validationProblems = {
|
||||||
sizeCheck: {
|
sizeCheck: {
|
||||||
|
@ -327,13 +291,12 @@ describe('<PdfPreview/>', function () {
|
||||||
|
|
||||||
mockValidationProblems(validationProblems)
|
mockValidationProblems(validationProblems)
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
// wait for "compile on load" to finish
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
screen.getByText('Project too large')
|
screen.getByText('Project too large')
|
||||||
screen.getByText('Unknown main document')
|
screen.getByText('Unknown main document')
|
||||||
screen.getByText('Conflicting Paths Found')
|
screen.getByText('Conflicting Paths Found')
|
||||||
|
@ -347,24 +310,18 @@ describe('<PdfPreview/>', function () {
|
||||||
mockBuildFile()
|
mockBuildFile()
|
||||||
mockClearCache()
|
mockClearCache()
|
||||||
|
|
||||||
renderWithEditorContext(<PdfPreview />)
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
const logsButton = screen.getByRole('button', { name: 'View logs' })
|
// wait for "compile on load" to finish
|
||||||
logsButton.click()
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
|
|
||||||
let clearCacheButton = screen.getByRole('button', {
|
|
||||||
name: 'Clear cached files',
|
|
||||||
})
|
|
||||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
|
||||||
|
|
||||||
// fire a project:joined event => compile
|
|
||||||
screen.getByRole('button', { name: 'Recompile' })
|
|
||||||
fireEvent(window, new CustomEvent('project:joined'))
|
|
||||||
screen.getByRole('button', { name: 'Compiling…' })
|
|
||||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
|
||||||
await screen.findByRole('button', { name: 'Recompile' })
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
|
const logsButton = screen.getByRole('button', {
|
||||||
|
name: 'This project has an error',
|
||||||
|
})
|
||||||
logsButton.click()
|
logsButton.click()
|
||||||
clearCacheButton = await screen.findByRole('button', {
|
|
||||||
|
const clearCacheButton = await screen.findByRole('button', {
|
||||||
name: 'Clear cached files',
|
name: 'Clear cached files',
|
||||||
})
|
})
|
||||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
||||||
|
@ -379,4 +336,43 @@ describe('<PdfPreview/>', function () {
|
||||||
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
|
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
|
||||||
expect(fetchMock.called('express:/build/:file')).to.be.true // TODO: actual path
|
expect(fetchMock.called('express:/build/:file')).to.be.true // TODO: actual path
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('handle "recompile from scratch"', async function () {
|
||||||
|
mockCompile()
|
||||||
|
mockBuildFile()
|
||||||
|
mockClearCache()
|
||||||
|
|
||||||
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
|
// wait for "compile on load" to finish
|
||||||
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
|
// show the logs UI
|
||||||
|
const logsButton = screen.getByRole('button', {
|
||||||
|
name: 'This project has an error',
|
||||||
|
})
|
||||||
|
logsButton.click()
|
||||||
|
|
||||||
|
const clearCacheButton = await screen.findByRole('button', {
|
||||||
|
name: 'Clear cached files',
|
||||||
|
})
|
||||||
|
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
||||||
|
|
||||||
|
const recompileFromScratch = screen.getByRole('menuitem', {
|
||||||
|
name: 'Recompile from scratch',
|
||||||
|
hidden: true,
|
||||||
|
})
|
||||||
|
recompileFromScratch.click()
|
||||||
|
|
||||||
|
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
||||||
|
|
||||||
|
// wait for compile to finish
|
||||||
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
|
expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param
|
||||||
|
expect(fetchMock.called('express:/project/:projectId/output')).to.be.true
|
||||||
|
expect(fetchMock.called('express:/build/:file')).to.be.true // TODO: actual path
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue