mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Migrate synctex controls to React (#5503)
GitOrigin-RevId: 80362a00ae6b73616a6fa9b3193b9b9974b5fd35
This commit is contained in:
parent
684efaaf5f
commit
913a62fbc8
20 changed files with 858 additions and 364 deletions
|
@ -24,28 +24,34 @@ div.full-size(
|
|||
else
|
||||
include ./pdf
|
||||
|
||||
.ui-layout-resizer-controls.synctex-controls(
|
||||
ng-show="!!pdf.url && settings.pdfViewer !== 'native'"
|
||||
ng-controller="PdfSynctexController"
|
||||
)
|
||||
a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-pdf(
|
||||
tooltip=translate('go_to_code_location_in_pdf')
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true"
|
||||
ng-click="syncToPdf()"
|
||||
ng-disabled="syncToPdfInFlight"
|
||||
if showNewPdfPreview
|
||||
.ui-layout-resizer-controls.synctex-controls(
|
||||
ng-show="settings.pdfViewer !== 'native'"
|
||||
)
|
||||
i.synctex-control-icon(ng-show="!syncToPdfInFlight")
|
||||
i.synctex-spin-icon.fa.fa-refresh.fa-spin(ng-show="syncToPdfInFlight")
|
||||
a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-code(
|
||||
tooltip=translate('go_to_pdf_location_in_code')
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true"
|
||||
ng-click="syncToCode()"
|
||||
ng-disabled="syncToCodeInFlight"
|
||||
pdf-synctex-controls()
|
||||
else
|
||||
.ui-layout-resizer-controls.synctex-controls(
|
||||
ng-show="!!pdf.url && settings.pdfViewer !== 'native'"
|
||||
ng-controller="PdfSynctexController"
|
||||
)
|
||||
i.synctex-control-icon(ng-show="!syncToCodeInFlight")
|
||||
i.synctex-spin-icon.fa.fa-refresh.fa-spin(ng-show="syncToCodeInFlight")
|
||||
a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-pdf(
|
||||
tooltip=translate('go_to_code_location_in_pdf')
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true"
|
||||
ng-click="syncToPdf()"
|
||||
ng-disabled="syncToPdfInFlight"
|
||||
)
|
||||
i.synctex-control-icon(ng-show="!syncToPdfInFlight")
|
||||
i.synctex-spin-icon.fa.fa-refresh.fa-spin(ng-show="syncToPdfInFlight")
|
||||
a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-code(
|
||||
tooltip=translate('go_to_pdf_location_in_code')
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true"
|
||||
ng-click="syncToCode()"
|
||||
ng-disabled="syncToCodeInFlight"
|
||||
)
|
||||
i.synctex-control-icon(ng-show="!syncToCodeInFlight")
|
||||
i.synctex-spin-icon.fa.fa-refresh.fa-spin(ng-show="syncToCodeInFlight")
|
||||
|
||||
div.full-size(
|
||||
ng-if="ui.pdfLayout == 'flat'"
|
||||
|
|
|
@ -122,7 +122,9 @@
|
|||
"go_next_page": "",
|
||||
"go_page": "",
|
||||
"go_prev_page": "",
|
||||
"go_to_code_location_in_pdf": "",
|
||||
"go_to_error_location": "",
|
||||
"go_to_pdf_location_in_code": "",
|
||||
"have_an_extra_backup": "",
|
||||
"headers": "",
|
||||
"hide_outline": "",
|
||||
|
|
|
@ -4,7 +4,6 @@ import { debounce } from 'lodash'
|
|||
import PdfViewerControls from './pdf-viewer-controls'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { buildHighlightElement } from '../util/highlights'
|
||||
import PDFJSWrapper from '../util/pdf-js-wrapper'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
|
@ -15,7 +14,12 @@ import getMeta from '../../../utils/meta'
|
|||
function PdfJsViewer({ url }) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const { setError, firstRenderDone } = useCompileContext()
|
||||
const {
|
||||
setError,
|
||||
firstRenderDone,
|
||||
highlights,
|
||||
setPosition,
|
||||
} = useCompileContext()
|
||||
const [timePDFFetched, setTimePDFFetched] = useState()
|
||||
|
||||
// state values persisted in localStorage to restore on load
|
||||
|
@ -24,10 +28,6 @@ function PdfJsViewer({ url }) {
|
|||
'page-width'
|
||||
)
|
||||
|
||||
// state values shared with Angular scope (highlights => editor, position => synctex buttons
|
||||
const [highlights] = useScopeValue('pdf.highlights')
|
||||
const [, setPosition] = useScopeValue('pdf.position')
|
||||
|
||||
// local state values
|
||||
const [pdfJsWrapper, setPdfJsWrapper] = useState()
|
||||
const [initialised, setInitialised] = useState(false)
|
||||
|
@ -144,16 +144,16 @@ function PdfJsViewer({ url }) {
|
|||
useEffect(() => {
|
||||
if (initialised && pdfJsWrapper) {
|
||||
setScale(scale => {
|
||||
pdfJsWrapper.viewer.currentScaleValue = scale
|
||||
return scale
|
||||
})
|
||||
setPosition(position => {
|
||||
if (position) {
|
||||
pdfJsWrapper.scrollToPosition(position, scale)
|
||||
} else {
|
||||
pdfJsWrapper.viewer.currentScaleValue = scale
|
||||
}
|
||||
return position
|
||||
})
|
||||
|
||||
// restore the scroll position
|
||||
setPosition(position => {
|
||||
if (position) {
|
||||
pdfJsWrapper.currentPosition = position
|
||||
}
|
||||
return position
|
||||
return scale
|
||||
})
|
||||
}
|
||||
}, [initialised, setScale, setPosition, pdfJsWrapper])
|
||||
|
|
|
@ -3,19 +3,28 @@ import PropTypes from 'prop-types'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewLogsPaneMaxEntries from '../../preview/components/preview-logs-pane-max-entries'
|
||||
import PdfLogEntry from './pdf-log-entry'
|
||||
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||
|
||||
const LOG_PREVIEW_LIMIT = 100
|
||||
|
||||
function PdfLogsEntries({ entries }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const syncToEntry = useCallback(entry => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('synctex:sync-to-entry', {
|
||||
detail: entry,
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
const ide = useIdeContext()
|
||||
|
||||
const syncToEntry = useCallback(
|
||||
entry => {
|
||||
const entity = ide.fileTreeManager.findEntityByPath(entry.file)
|
||||
|
||||
if (entity && entity.type === 'doc') {
|
||||
ide.editorManager.openDoc(entity, {
|
||||
gotoLine: entry.line ?? undefined,
|
||||
gotoColumn: entry.column ?? undefined,
|
||||
})
|
||||
}
|
||||
},
|
||||
[ide]
|
||||
)
|
||||
|
||||
const logEntries = entries.slice(0, LOG_PREVIEW_LIMIT)
|
||||
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { getJSON } from '../../../infrastructure/fetch-json'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useIsMounted from '../../../shared/hooks/use-is-mounted'
|
||||
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
||||
|
||||
function PdfSynctexControls() {
|
||||
const ide = useIdeContext()
|
||||
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const {
|
||||
clsiServerId,
|
||||
pdfUrl,
|
||||
pdfViewer,
|
||||
position,
|
||||
setHighlights,
|
||||
} = useCompileContext()
|
||||
|
||||
const [cursorPosition, setCursorPosition] = useState(null)
|
||||
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const { signal } = useAbortController()
|
||||
|
||||
useEffect(() => {
|
||||
const listener = event => setCursorPosition(event.detail)
|
||||
window.addEventListener('cursor:editor:update', listener)
|
||||
return () => window.removeEventListener('cursor:editor:update', listener)
|
||||
}, [ide])
|
||||
|
||||
const [syncToPdfInFlight, setSyncToPdfInFlight] = useState(false)
|
||||
const [syncToCodeInFlight, setSyncToCodeInFlight] = useState(false)
|
||||
|
||||
const [, setSynctexError] = useScopeValue('sync_tex_error')
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getCurrentFilePath = useCallback(() => {
|
||||
const docId = ide.editorManager.getCurrentDocId()
|
||||
const doc = ide.fileTreeManager.findEntityById(docId)
|
||||
|
||||
let path = ide.fileTreeManager.getEntityPath(doc)
|
||||
|
||||
// If the root file is folder/main.tex, then synctex sees the path as folder/./main.tex
|
||||
const rootDocDirname = ide.fileTreeManager.getRootDocDirname()
|
||||
|
||||
if (rootDocDirname) {
|
||||
path = path.replace(RegExp(`^${rootDocDirname}`), `${rootDocDirname}/.`)
|
||||
}
|
||||
|
||||
return path
|
||||
}, [ide])
|
||||
|
||||
const syncToPdf = useCallback(
|
||||
cursorPosition => {
|
||||
setSyncToPdfInFlight(true)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
file: getCurrentFilePath(),
|
||||
line: cursorPosition.row + 1,
|
||||
column: cursorPosition.column,
|
||||
})
|
||||
|
||||
if (clsiServerId) {
|
||||
params.set('clsiserverid', clsiServerId)
|
||||
}
|
||||
|
||||
getJSON(`/project/${projectId}/sync/code?${params}`, { signal })
|
||||
.then(data => {
|
||||
setHighlights(data.pdf)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
.finally(() => {
|
||||
if (isMounted.current) {
|
||||
setSyncToPdfInFlight(false)
|
||||
}
|
||||
})
|
||||
},
|
||||
[
|
||||
clsiServerId,
|
||||
projectId,
|
||||
setHighlights,
|
||||
getCurrentFilePath,
|
||||
signal,
|
||||
isMounted,
|
||||
]
|
||||
)
|
||||
|
||||
const syncToCode = useCallback(
|
||||
(position, visualOffset = 0) => {
|
||||
// FIXME: this actually works better if it's halfway across the
|
||||
// page (or the visible part of the page). Synctex doesn't
|
||||
// always find the right place in the file when the point is at
|
||||
// the edge of the page, it sometimes returns the start of the
|
||||
// next paragraph instead.
|
||||
const h = position.offset.left
|
||||
|
||||
// Compute the vertical position to pass to synctex, which
|
||||
// works with coordinates increasing from the top of the page
|
||||
// down. This matches the browser's DOM coordinate of the
|
||||
// click point, but the pdf position is measured from the
|
||||
// bottom of the page so we need to invert it.
|
||||
let v = 0
|
||||
if (position.pageSize?.height) {
|
||||
v += position.pageSize.height - position.offset.top // measure from pdf point (inverted)
|
||||
} else {
|
||||
v += position.offset.top // measure from html click position
|
||||
}
|
||||
v += visualOffset
|
||||
|
||||
setSyncToCodeInFlight(true)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: position.page + 1,
|
||||
h: h.toFixed(2),
|
||||
v: v.toFixed(2),
|
||||
})
|
||||
|
||||
if (clsiServerId) {
|
||||
params.set('clsiserverid', clsiServerId)
|
||||
}
|
||||
|
||||
getJSON(`/project/${projectId}/sync/pdf?${params}`, { signal })
|
||||
.then(data => {
|
||||
const [{ file, line }] = data.code
|
||||
if (file) {
|
||||
const doc = ide.fileTreeManager.findEntityByPath(file)
|
||||
|
||||
ide.editorManager.openDoc(doc, {
|
||||
gotoLine: line,
|
||||
})
|
||||
} else {
|
||||
setSynctexError(true)
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (isMounted.current) {
|
||||
setSynctexError(false)
|
||||
}
|
||||
}, 4000)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
.finally(() => {
|
||||
if (isMounted.current) {
|
||||
setSyncToCodeInFlight(false)
|
||||
}
|
||||
})
|
||||
},
|
||||
[clsiServerId, ide, projectId, setSynctexError, signal, isMounted]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const listener = event => syncToCode(event.detail)
|
||||
window.addEventListener('synctex:sync-to-position', listener)
|
||||
return () => {
|
||||
window.removeEventListener('synctex:sync-to-position', listener)
|
||||
}
|
||||
}, [syncToCode])
|
||||
|
||||
if (!pdfUrl || pdfViewer === 'native') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id="sync-to-pdf-tooltip">
|
||||
{t('go_to_code_location_in_pdf')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
bsStyle="default"
|
||||
bsSize="xs"
|
||||
onClick={() => syncToPdf(cursorPosition)}
|
||||
disabled={syncToPdfInFlight}
|
||||
className="synctex-control"
|
||||
aria-label={t('go_to_code_location_in_pdf')}
|
||||
>
|
||||
{syncToPdfInFlight ? (
|
||||
<Icon type="refresh" spin classes={{ icon: 'synctex-spin-icon' }} />
|
||||
) : (
|
||||
<Icon
|
||||
type="arrow-right"
|
||||
classes={{ icon: 'synctex-control-icon' }}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id="sync-to-code-tooltip">
|
||||
{t('go_to_pdf_location_in_code')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
bsStyle="default"
|
||||
bsSize="xs"
|
||||
onClick={() => syncToCode(position, 72)}
|
||||
disabled={syncToCodeInFlight}
|
||||
className="synctex-control"
|
||||
aria-label={t('go_to_pdf_location_in_code')}
|
||||
>
|
||||
{syncToCodeInFlight ? (
|
||||
<Icon type="refresh" spin classes={{ icon: 'synctex-spin-icon' }} />
|
||||
) : (
|
||||
<Icon
|
||||
type="arrow-left"
|
||||
classes={{ icon: 'synctex-control-icon' }}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PdfSynctexControls)
|
|
@ -1,25 +1,12 @@
|
|||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { lazy, memo, useEffect } from 'react'
|
||||
import { lazy, memo } from 'react'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
|
||||
const PdfJsViewer = lazy(() =>
|
||||
import(/* webpackChunkName: "pdf-js-viewer" */ './pdf-js-viewer')
|
||||
)
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
function PdfViewer() {
|
||||
const [pdfViewer, setPdfViewer] = useScopeValue('settings.pdfViewer')
|
||||
|
||||
useEffect(() => {
|
||||
const viewer = params.get('viewer')
|
||||
|
||||
if (viewer) {
|
||||
setPdfViewer(viewer)
|
||||
}
|
||||
}, [setPdfViewer])
|
||||
|
||||
const { pdfUrl } = useCompileContext()
|
||||
const { pdfUrl, pdfViewer } = useCompileContext()
|
||||
|
||||
if (!pdfUrl) {
|
||||
return null
|
||||
|
|
|
@ -3,5 +3,10 @@ import { react2angular } from 'react2angular'
|
|||
|
||||
import PdfPreview from '../components/pdf-preview'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import PdfSynctexControls from '../components/pdf-synctex-controls'
|
||||
|
||||
App.component('pdfPreview', react2angular(rootContext.use(PdfPreview), []))
|
||||
App.component(
|
||||
'pdfSynctexControls',
|
||||
react2angular(rootContext.use(PdfSynctexControls), [])
|
||||
)
|
||||
|
|
|
@ -160,7 +160,7 @@ export default class PDFJSWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
set currentPosition(position) {
|
||||
scrollToPosition(position, scale = null) {
|
||||
const destArray = [
|
||||
null,
|
||||
{
|
||||
|
@ -168,7 +168,7 @@ export default class PDFJSWrapper {
|
|||
},
|
||||
position.offset.left,
|
||||
position.offset.top,
|
||||
null,
|
||||
scale,
|
||||
]
|
||||
|
||||
this.viewer.scrollPageIntoView({
|
||||
|
|
|
@ -99,7 +99,12 @@ export default CursorPositionManager = class CursorPositionManager {
|
|||
|
||||
emitCursorUpdateEvent() {
|
||||
const cursor = this.adapter.getCursor()
|
||||
return this.$scope.$emit(`cursor:${this.$scope.name}:update`, cursor)
|
||||
this.$scope.$emit(`cursor:${this.$scope.name}:update`, cursor)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`cursor:${this.$scope.name}:update`, {
|
||||
detail: cursor,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
gotoStoredPosition() {
|
||||
|
|
|
@ -38,15 +38,20 @@ CompileContext.Provider.propTypes = {
|
|||
fileList: PropTypes.object,
|
||||
hasChanges: PropTypes.bool.isRequired,
|
||||
hasLintingError: PropTypes.bool,
|
||||
highlights: PropTypes.arrayOf(PropTypes.object),
|
||||
logEntries: PropTypes.object,
|
||||
logEntryAnnotations: PropTypes.object,
|
||||
pdfDownloadUrl: PropTypes.string,
|
||||
pdfUrl: PropTypes.string,
|
||||
pdfViewer: PropTypes.string,
|
||||
position: PropTypes.object,
|
||||
rawLog: PropTypes.string,
|
||||
setAutoCompile: PropTypes.func.isRequired,
|
||||
setDraft: PropTypes.func.isRequired,
|
||||
setError: PropTypes.func.isRequired,
|
||||
setHasLintingError: PropTypes.func.isRequired, // only for storybook
|
||||
setHighlights: PropTypes.func.isRequired,
|
||||
setPosition: PropTypes.func.isRequired,
|
||||
setShowLogs: PropTypes.func.isRequired,
|
||||
setStopOnValidationError: PropTypes.func.isRequired,
|
||||
showLogs: PropTypes.bool.isRequired,
|
||||
|
@ -70,24 +75,27 @@ export function CompileProvider({ children }) {
|
|||
const [compiling, setCompiling] = useState(false)
|
||||
|
||||
// the log entries parsed from the compile output log
|
||||
const [logEntries, setLogEntries] = useScopeValue('pdf.logEntries')
|
||||
const [logEntries, setLogEntries] = useState()
|
||||
|
||||
// annotations for display in the editor, built from the log entries
|
||||
const [logEntryAnnotations, setLogEntryAnnotations] = useScopeValue(
|
||||
'pdf.logEntryAnnotations'
|
||||
)
|
||||
|
||||
// the PDF viewer
|
||||
const [pdfViewer] = useScopeValue('settings.pdfViewer')
|
||||
|
||||
// the URL for downloading the PDF
|
||||
const [pdfDownloadUrl, setPdfDownloadUrl] = useScopeValue('pdf.downloadUrl')
|
||||
const [pdfDownloadUrl, setPdfDownloadUrl] = useState()
|
||||
|
||||
// the URL for loading the PDF in the preview pane
|
||||
const [pdfUrl, setPdfUrl] = useScopeValue('pdf.url')
|
||||
const [pdfUrl, setPdfUrl] = useState()
|
||||
|
||||
// the project is considered to be "uncompiled" if a doc has changed since the last compile started
|
||||
const [uncompiled, setUncompiled] = useScopeValue('pdf.uncompiled')
|
||||
const [uncompiled, setUncompiled] = useState()
|
||||
|
||||
// the id of the CLSI server which ran the compile
|
||||
const [clsiServerId, setClsiServerId] = useScopeValue('pdf.clsiServerId')
|
||||
const [clsiServerId, setClsiServerId] = useState()
|
||||
|
||||
// data received in response to a compile request
|
||||
const [data, setData] = useState()
|
||||
|
@ -116,6 +124,12 @@ export function CompileProvider({ children }) {
|
|||
// validation issues from CLSI
|
||||
const [validationIssues, setValidationIssues] = useState()
|
||||
|
||||
// areas to highlight on the PDF, from synctex
|
||||
const [highlights, setHighlights] = useState()
|
||||
|
||||
// scroll position of the PDF
|
||||
const [position, setPosition] = usePersistedState(`pdf.position.${projectId}`)
|
||||
|
||||
// whether autocompile is switched on
|
||||
const [autoCompile, _setAutoCompile] = usePersistedState(
|
||||
`autocompile_enabled:${projectId}`,
|
||||
|
@ -331,18 +345,18 @@ export function CompileProvider({ children }) {
|
|||
|
||||
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)
|
||||
const canAutoCompile = Boolean(autoCompile && !codeCheckFailed)
|
||||
|
||||
// show that the project has pending changes
|
||||
const hasChanges = Boolean(canAutoCompile && uncompiled && compiledOnce)
|
||||
|
||||
// call the debounced autocompile function if the project is available for auto-compiling and it has changed
|
||||
useEffect(() => {
|
||||
if (canAutoCompile && changedAt > 0) {
|
||||
compiler.debouncedAutoCompile()
|
||||
if (canAutoCompile) {
|
||||
if (changedAt > 0) {
|
||||
compiler.debouncedAutoCompile()
|
||||
}
|
||||
} else {
|
||||
compiler.debouncedAutoCompile.cancel()
|
||||
}
|
||||
|
@ -409,10 +423,13 @@ export function CompileProvider({ children }) {
|
|||
fileList,
|
||||
hasChanges,
|
||||
hasLintingError,
|
||||
highlights,
|
||||
logEntries,
|
||||
logEntryAnnotations,
|
||||
pdfDownloadUrl,
|
||||
pdfUrl,
|
||||
pdfViewer,
|
||||
position,
|
||||
rawLog,
|
||||
recompileFromScratch,
|
||||
setAutoCompile,
|
||||
|
@ -421,6 +438,8 @@ export function CompileProvider({ children }) {
|
|||
setDraft,
|
||||
setError,
|
||||
setHasLintingError, // only for stories
|
||||
setHighlights,
|
||||
setPosition,
|
||||
setShowLogs,
|
||||
setStopOnValidationError,
|
||||
showLogs,
|
||||
|
@ -443,16 +462,21 @@ export function CompileProvider({ children }) {
|
|||
fileList,
|
||||
hasChanges,
|
||||
hasLintingError,
|
||||
highlights,
|
||||
logEntries,
|
||||
logEntryAnnotations,
|
||||
position,
|
||||
pdfDownloadUrl,
|
||||
pdfUrl,
|
||||
pdfViewer,
|
||||
rawLog,
|
||||
recompileFromScratch,
|
||||
setAutoCompile,
|
||||
setDraft,
|
||||
setError,
|
||||
setHasLintingError,
|
||||
setHighlights,
|
||||
setPosition,
|
||||
setStopOnValidationError,
|
||||
showLogs,
|
||||
startCompile,
|
||||
|
|
228
services/web/frontend/stories/fixtures/compile.js
Normal file
228
services/web/frontend/stories/fixtures/compile.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
import examplePdf from './storybook-example.pdf'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
export const dispatchDocChanged = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('doc:changed', { detail: { doc_id: 'foo' } })
|
||||
)
|
||||
}
|
||||
|
||||
export const outputFiles = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
build: '123',
|
||||
url: '/build/output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'output.bbl',
|
||||
build: '123',
|
||||
url: '/build/output.bbl',
|
||||
type: 'bbl',
|
||||
},
|
||||
{
|
||||
path: 'output.bib',
|
||||
build: '123',
|
||||
url: '/build/output.bib',
|
||||
type: 'bib',
|
||||
},
|
||||
{
|
||||
path: 'example.txt',
|
||||
build: '123',
|
||||
url: '/build/example.txt',
|
||||
type: 'txt',
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
build: '123',
|
||||
url: '/build/output.log',
|
||||
type: 'log',
|
||||
},
|
||||
{
|
||||
path: 'output.blg',
|
||||
build: '123',
|
||||
url: '/build/output.blg',
|
||||
type: 'blg',
|
||||
},
|
||||
]
|
||||
|
||||
export const mockCompile = (fetchMock, delay = 1000) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
{
|
||||
body: {
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
pdfDownloadDomain: '',
|
||||
outputFiles: cloneDeep(outputFiles),
|
||||
},
|
||||
},
|
||||
{ delay }
|
||||
)
|
||||
|
||||
export const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
{
|
||||
body: {
|
||||
status,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
},
|
||||
{ delay, overwriteRoutes: true }
|
||||
)
|
||||
|
||||
export const mockCompileValidationIssues = (
|
||||
fetchMock,
|
||||
validationProblems,
|
||||
delay = 1000
|
||||
) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
() => {
|
||||
return {
|
||||
body: {
|
||||
status: 'validation-problems',
|
||||
validationProblems,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
}
|
||||
},
|
||||
{ delay, overwriteRoutes: true }
|
||||
)
|
||||
|
||||
export const mockClearCache = fetchMock =>
|
||||
fetchMock.delete('express:/project/:projectId/output', 204, {
|
||||
delay: 1000,
|
||||
})
|
||||
|
||||
export const mockBuildFile = fetchMock =>
|
||||
fetchMock.get(
|
||||
'express:/build/:file',
|
||||
(url, options, request) => {
|
||||
const { pathname } = new URL(url, 'https://example.com')
|
||||
|
||||
switch (pathname) {
|
||||
case '/build/output.blg':
|
||||
return 'This is BibTeX, Version 4.0' // FIXME
|
||||
|
||||
case '/build/output.log':
|
||||
return `
|
||||
The LaTeX compiler output
|
||||
* With a lot of details
|
||||
|
||||
Wrapped in an HTML <pre> element with
|
||||
preformatted text which is to be presented exactly
|
||||
as written in the HTML file
|
||||
|
||||
(whitespace included™)
|
||||
|
||||
The text is typically rendered using a non-proportional ("monospace") font.
|
||||
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <7> on input line 18.
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <5> on input line 18.
|
||||
! Undefined control sequence.
|
||||
<recently read> \\Zlpha
|
||||
|
||||
main.tex, line 23
|
||||
|
||||
`
|
||||
|
||||
case '/build/output.pdf':
|
||||
return new Promise(resolve => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
resolve({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||
},
|
||||
body: xhr.response,
|
||||
})
|
||||
})
|
||||
xhr.open('GET', examplePdf)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.send()
|
||||
})
|
||||
|
||||
default:
|
||||
console.log(pathname)
|
||||
return 404
|
||||
}
|
||||
},
|
||||
{ sendAsJson: false }
|
||||
)
|
||||
|
||||
const mockHighlights = [
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 509.999878,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 486.089539,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 498.044708,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 521.955078,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
]
|
||||
|
||||
export const mockEventTracking = fetchMock =>
|
||||
fetchMock.get('express:/event/:event', 204)
|
||||
|
||||
export const mockValidPdf = fetchMock =>
|
||||
fetchMock.get(
|
||||
'express:/build/output.pdf',
|
||||
(url, options, request) => {
|
||||
return new Promise(resolve => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
resolve({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||
'Accept-Ranges': 'bytes',
|
||||
},
|
||||
body: xhr.response,
|
||||
})
|
||||
})
|
||||
xhr.open('GET', examplePdf)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.send()
|
||||
})
|
||||
},
|
||||
{ sendAsJson: false }
|
||||
)
|
||||
|
||||
export const mockSynctex = fetchMock =>
|
||||
fetchMock
|
||||
.get('express:/project/:projectId/sync/code', () => {
|
||||
return { pdf: cloneDeep(mockHighlights) }
|
||||
})
|
||||
.get('express:/project/:projectId/sync/pdf', () => {
|
||||
return { code: [{ file: 'main.tex', line: 100 }] }
|
||||
})
|
|
@ -18,6 +18,9 @@ export function setupContext() {
|
|||
chatOpen: true,
|
||||
pdfLayout: 'flat',
|
||||
},
|
||||
settings: {
|
||||
pdfViewer: 'js',
|
||||
},
|
||||
toggleHistory: () => {},
|
||||
}
|
||||
}
|
||||
|
@ -29,9 +32,17 @@ export function setupContext() {
|
|||
removeListener: sinon.stub(),
|
||||
},
|
||||
fileTreeManager: {
|
||||
findEntityById: () => null,
|
||||
findEntityByPath: () => null,
|
||||
getEntityPath: () => null,
|
||||
getRootDocDirname: () => undefined,
|
||||
},
|
||||
editorManager: {
|
||||
getCurrentDocId: () => 'foo',
|
||||
openDoc: (id, options) => {
|
||||
console.log('open doc', id, options)
|
||||
},
|
||||
},
|
||||
}
|
||||
window.ExposedSettings = window.ExposedSettings || {}
|
||||
window.ExposedSettings.appName = 'Overleaf'
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import PdfJsViewer from '../js/features/pdf-preview/components/pdf-js-viewer'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import examplePdf from './fixtures/storybook-example.pdf'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useCallback } from 'react'
|
||||
import { withContextRoot } from './utils/with-context-root'
|
||||
import { setupContext } from './fixtures/context'
|
||||
import useScopeValue from '../js/shared/hooks/use-scope-value'
|
||||
|
||||
setupContext()
|
||||
|
||||
export default {
|
||||
title: 'PDF Viewer',
|
||||
component: PdfJsViewer,
|
||||
}
|
||||
|
||||
const project = {
|
||||
_id: 'story-project',
|
||||
}
|
||||
|
||||
const mockHighlights = [
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 509.999878,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 486.089539,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 498.044708,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 521.955078,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
]
|
||||
|
||||
export const Interactive = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.get(
|
||||
'express:/build/output.pdf',
|
||||
(url, options, request) => {
|
||||
return new Promise(resolve => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
resolve({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||
'Accept-Ranges': 'bytes',
|
||||
},
|
||||
body: xhr.response,
|
||||
})
|
||||
})
|
||||
xhr.open('GET', examplePdf)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.send()
|
||||
})
|
||||
},
|
||||
{ sendAsJson: false }
|
||||
)
|
||||
})
|
||||
|
||||
const Inner = () => {
|
||||
const [, setHighlights] = useScopeValue('pdf.highlights')
|
||||
|
||||
const dispatchSyncFromCode = useCallback(() => {
|
||||
setHighlights([])
|
||||
window.setTimeout(() => {
|
||||
setHighlights(mockHighlights)
|
||||
}, 0)
|
||||
}, [setHighlights])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
zIndex: 10,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
right: 40,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 20 }}>
|
||||
<Button onClick={dispatchSyncFromCode}>
|
||||
sync position from editor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return withContextRoot(
|
||||
<div className="pdf-viewer">
|
||||
<PdfJsViewer url="/build/output.pdf" />
|
||||
<Inner />
|
||||
</div>,
|
||||
{ project }
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { withContextRoot } from './utils/with-context-root'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import { setupContext } from './fixtures/context'
|
||||
import { Button } from 'react-bootstrap'
|
||||
|
@ -9,10 +9,20 @@ import PdfPreviewToolbar from '../js/features/pdf-preview/components/pdf-preview
|
|||
import PdfFileList from '../js/features/pdf-preview/components/pdf-file-list'
|
||||
import { buildFileList } from '../js/features/pdf-preview/util/file-list'
|
||||
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'
|
||||
import {
|
||||
dispatchDocChanged,
|
||||
mockBuildFile,
|
||||
mockClearCache,
|
||||
mockCompile,
|
||||
mockCompileError,
|
||||
mockCompileValidationIssues,
|
||||
mockEventTracking,
|
||||
outputFiles,
|
||||
} from './fixtures/compile'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
setupContext()
|
||||
|
||||
|
@ -55,167 +65,6 @@ const scope = {
|
|||
},
|
||||
}
|
||||
|
||||
const dispatchProjectJoined = () => {
|
||||
window.dispatchEvent(new CustomEvent('project:joined', { detail: project }))
|
||||
}
|
||||
|
||||
const dispatchDocChanged = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('doc:changed', { detail: { doc_id: 'foo' } })
|
||||
)
|
||||
}
|
||||
|
||||
const outputFiles = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
build: '123',
|
||||
url: '/build/output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'output.bbl',
|
||||
build: '123',
|
||||
url: '/build/output.bbl',
|
||||
type: 'bbl',
|
||||
},
|
||||
{
|
||||
path: 'output.bib',
|
||||
build: '123',
|
||||
url: '/build/output.bib',
|
||||
type: 'bib',
|
||||
},
|
||||
{
|
||||
path: 'example.txt',
|
||||
build: '123',
|
||||
url: '/build/example.txt',
|
||||
type: 'txt',
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
build: '123',
|
||||
url: '/build/output.log',
|
||||
type: 'log',
|
||||
},
|
||||
{
|
||||
path: 'output.blg',
|
||||
build: '123',
|
||||
url: '/build/output.blg',
|
||||
type: 'blg',
|
||||
},
|
||||
]
|
||||
|
||||
const mockCompile = (fetchMock, delay = 1000) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
{
|
||||
body: {
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
pdfDownloadDomain: '',
|
||||
outputFiles,
|
||||
},
|
||||
},
|
||||
{ delay }
|
||||
)
|
||||
|
||||
const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
{
|
||||
body: {
|
||||
status,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
},
|
||||
{ delay, overwriteRoutes: true }
|
||||
)
|
||||
|
||||
const mockCompileValidationIssues = (
|
||||
fetchMock,
|
||||
validationProblems,
|
||||
delay = 1000
|
||||
) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
() => {
|
||||
return {
|
||||
body: {
|
||||
status: 'validation-problems',
|
||||
validationProblems,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
}
|
||||
},
|
||||
{ delay, overwriteRoutes: true }
|
||||
)
|
||||
|
||||
const mockClearCache = fetchMock =>
|
||||
fetchMock.delete('express:/project/:projectId/output', 204, {
|
||||
delay: 1000,
|
||||
})
|
||||
|
||||
const mockBuildFile = fetchMock =>
|
||||
fetchMock.get(
|
||||
'express:/build/:file',
|
||||
(url, options, request) => {
|
||||
const { pathname } = new URL(url, 'https://example.com')
|
||||
|
||||
switch (pathname) {
|
||||
case '/build/output.blg':
|
||||
return 'This is BibTeX, Version 4.0' // FIXME
|
||||
|
||||
case '/build/output.log':
|
||||
return `
|
||||
The LaTeX compiler output
|
||||
* With a lot of details
|
||||
|
||||
Wrapped in an HTML <pre> element with
|
||||
preformatted text which is to be presented exactly
|
||||
as written in the HTML file
|
||||
|
||||
(whitespace included™)
|
||||
|
||||
The text is typically rendered using a non-proportional ("monospace") font.
|
||||
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <7> on input line 18.
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <5> on input line 18.
|
||||
! Undefined control sequence.
|
||||
<recently read> \\Zlpha
|
||||
|
||||
main.tex, line 23
|
||||
|
||||
`
|
||||
|
||||
case '/build/output.pdf':
|
||||
return new Promise(resolve => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
resolve({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||
},
|
||||
body: xhr.response,
|
||||
})
|
||||
})
|
||||
xhr.open('GET', examplePdf)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.send()
|
||||
})
|
||||
|
||||
default:
|
||||
return 404
|
||||
}
|
||||
},
|
||||
{ sendAsJson: false }
|
||||
)
|
||||
|
||||
export const Interactive = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
|
@ -223,10 +72,6 @@ export const Interactive = () => {
|
|||
mockClearCache(fetchMock)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
dispatchProjectJoined()
|
||||
}, [])
|
||||
|
||||
const Inner = () => {
|
||||
const context = useCompileContext()
|
||||
|
||||
|
@ -465,6 +310,10 @@ const compileErrors = [
|
|||
]
|
||||
|
||||
export const DisplayError = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
})
|
||||
|
||||
return withContextRoot(
|
||||
<>
|
||||
{compileErrors.map(error => (
|
||||
|
@ -482,7 +331,10 @@ export const DisplayError = () => {
|
|||
}
|
||||
|
||||
export const Toolbar = () => {
|
||||
useFetchMock(fetchMock => mockCompile(fetchMock, 500))
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock, 500)
|
||||
mockBuildFile(fetchMock)
|
||||
})
|
||||
|
||||
return withContextRoot(
|
||||
<div className="pdf">
|
||||
|
@ -496,6 +348,7 @@ export const HybridToolbar = () => {
|
|||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock, 500)
|
||||
mockBuildFile(fetchMock)
|
||||
mockEventTracking(fetchMock)
|
||||
})
|
||||
|
||||
return withContextRoot(
|
||||
|
@ -508,7 +361,7 @@ export const HybridToolbar = () => {
|
|||
|
||||
export const FileList = () => {
|
||||
const fileList = useMemo(() => {
|
||||
return buildFileList(outputFiles)
|
||||
return buildFileList(cloneDeep(outputFiles))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@ -559,9 +412,5 @@ export const ValidationIssues = () => {
|
|||
mockBuildFile(fetchMock)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
dispatchProjectJoined()
|
||||
}, [])
|
||||
|
||||
return withContextRoot(<PdfPreviewPane />, scope)
|
||||
}
|
||||
|
|
62
services/web/frontend/stories/pdf-viewer.stories.js
Normal file
62
services/web/frontend/stories/pdf-viewer.stories.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import { withContextRoot } from './utils/with-context-root'
|
||||
import { setupContext } from './fixtures/context'
|
||||
import PdfSynctexControls from '../js/features/pdf-preview/components/pdf-synctex-controls'
|
||||
import PdfViewer from '../js/features/pdf-preview/components/pdf-viewer'
|
||||
import {
|
||||
mockBuildFile,
|
||||
mockCompile,
|
||||
mockSynctex,
|
||||
mockValidPdf,
|
||||
} from './fixtures/compile'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
setupContext()
|
||||
|
||||
export default {
|
||||
title: 'PDF Viewer',
|
||||
component: PdfViewer,
|
||||
}
|
||||
|
||||
const project = {
|
||||
_id: 'story-project',
|
||||
}
|
||||
|
||||
const scope = {
|
||||
project,
|
||||
editor: {
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Interactive = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
mockBuildFile(fetchMock)
|
||||
mockValidPdf(fetchMock)
|
||||
mockSynctex(fetchMock)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`cursor:editor:update`, {
|
||||
detail: { row: 10, position: 10 },
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
|
||||
return withContextRoot(
|
||||
<div>
|
||||
<div className="pdf-viewer">
|
||||
<PdfViewer />
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 150, left: 50 }}>
|
||||
<PdfSynctexControls />
|
||||
</div>
|
||||
</div>,
|
||||
scope
|
||||
)
|
||||
}
|
|
@ -24,6 +24,9 @@ export function withContextRoot(Story, scope) {
|
|||
}
|
||||
}, 0)
|
||||
},
|
||||
$on: (eventName, callback) => {
|
||||
//
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -362,6 +362,10 @@
|
|||
background-color: fade(@btn-default-bg, 80%);
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 1;
|
||||
background-color: fade(@btn-default-bg, 60%);
|
||||
|
|
|
@ -473,7 +473,7 @@ describe('<PdfPreview/>', function () {
|
|||
mockCompile()
|
||||
mockBuildFile()
|
||||
|
||||
nock('https://www.test-overleaf.com')
|
||||
nock('https://clsi.test-overleaf.com')
|
||||
.get(/^\/build\/output.pdf/)
|
||||
.replyWithError({
|
||||
message: 'something awful happened',
|
||||
|
@ -485,6 +485,8 @@ describe('<PdfPreview/>', function () {
|
|||
await screen.findByText('Something went wrong while rendering this PDF.')
|
||||
expect(screen.queryByLabelText('Page 1')).to.not.exist
|
||||
|
||||
expect(nock.isDone()).to.be.true
|
||||
|
||||
mockValidPdf()
|
||||
|
||||
rerender(<PdfPreview />)
|
||||
|
@ -498,7 +500,7 @@ describe('<PdfPreview/>', function () {
|
|||
mockCompile()
|
||||
mockBuildFile()
|
||||
|
||||
nock('https://www.test-overleaf.com')
|
||||
nock('https://clsi.test-overleaf.com')
|
||||
.get(/^\/build\/output.pdf/)
|
||||
.replyWithFile(200, corruptPDF)
|
||||
|
||||
|
@ -507,6 +509,8 @@ describe('<PdfPreview/>', function () {
|
|||
await screen.findByText('Something went wrong while rendering this PDF.')
|
||||
expect(screen.queryByLabelText('Page 1')).to.not.exist
|
||||
|
||||
expect(nock.isDone()).to.be.true
|
||||
|
||||
mockValidPdf()
|
||||
|
||||
rerender(<PdfPreview />)
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
import PdfSynctexControls from '../../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
|
||||
import { EditorProviders } from '../../../helpers/render-with-context'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { fireEvent, screen, waitFor, render } from '@testing-library/react'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { expect } from 'chai'
|
||||
import { useCompileContext } from '../../../../../frontend/js/shared/context/compile-context'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf')
|
||||
|
||||
const scope = {
|
||||
settings: {
|
||||
syntaxValidation: false,
|
||||
pdfViewer: 'pdfjs',
|
||||
},
|
||||
editor: {
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const outputFiles = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
build: '123',
|
||||
url: '/build/output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
build: '123',
|
||||
url: '/build/output.log',
|
||||
type: 'log',
|
||||
},
|
||||
]
|
||||
|
||||
const mockCompile = () =>
|
||||
fetchMock.post('express:/project/:projectId/compile', {
|
||||
body: {
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'standard',
|
||||
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
|
||||
outputFiles: cloneDeep(outputFiles),
|
||||
},
|
||||
})
|
||||
|
||||
const fileResponses = {
|
||||
'/build/output.pdf': () => fs.createReadStream(examplePDF),
|
||||
'/build/output.log': '',
|
||||
}
|
||||
|
||||
const mockBuildFile = () =>
|
||||
fetchMock.get('begin:https://clsi.test-overleaf.com/', _url => {
|
||||
const url = new URL(_url, 'https://clsi.test-overleaf.com')
|
||||
|
||||
if (url.pathname in fileResponses) {
|
||||
return fileResponses[url.pathname]
|
||||
}
|
||||
|
||||
return 404
|
||||
})
|
||||
|
||||
const mockHighlights = [
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 509.999878,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 486.089539,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
]
|
||||
|
||||
const mockSynctex = () =>
|
||||
fetchMock
|
||||
.get('express:/project/:projectId/sync/code', () => {
|
||||
return { pdf: cloneDeep(mockHighlights) }
|
||||
})
|
||||
.get('express:/project/:projectId/sync/pdf', () => {
|
||||
return { code: [{ file: 'main.tex', line: 100 }] }
|
||||
})
|
||||
|
||||
describe('<PdfSynctexControls/>', function () {
|
||||
beforeEach(function () {
|
||||
window.showNewPdfPreview = true
|
||||
fetchMock.restore()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.showNewPdfPreview = undefined
|
||||
fetchMock.restore()
|
||||
})
|
||||
|
||||
it('handles clicks on sync buttons', async function () {
|
||||
mockCompile()
|
||||
mockSynctex()
|
||||
mockBuildFile()
|
||||
|
||||
const Inner = () => {
|
||||
const { setPosition } = useCompileContext()
|
||||
|
||||
// mock PDF scroll position update
|
||||
useEffect(() => {
|
||||
setPosition({
|
||||
page: 1,
|
||||
offset: { top: 10, left: 10 },
|
||||
pageSize: { height: 500, width: 500 },
|
||||
})
|
||||
}, [setPosition])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
render(
|
||||
<EditorProviders scope={scope}>
|
||||
<Inner />
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
const syncToPdfButton = await screen.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
})
|
||||
|
||||
const syncToCodeButton = await screen.findByRole('button', {
|
||||
name: 'Go to PDF location in code',
|
||||
})
|
||||
|
||||
// mock editor cursor position update
|
||||
fireEvent(
|
||||
window,
|
||||
new CustomEvent('cursor:editor:update', {
|
||||
detail: { row: 100, column: 10 },
|
||||
})
|
||||
)
|
||||
|
||||
fireEvent.click(syncToPdfButton)
|
||||
|
||||
expect(syncToPdfButton.disabled).to.be.true
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
|
||||
.true
|
||||
})
|
||||
|
||||
fireEvent.click(syncToCodeButton)
|
||||
|
||||
expect(syncToCodeButton.disabled).to.be.true
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
|
||||
.true
|
||||
})
|
||||
})
|
||||
})
|
|
@ -48,20 +48,28 @@ export function EditorProviders({
|
|||
callback(get($scope, path))
|
||||
return () => null
|
||||
},
|
||||
$applyAsync: () => {},
|
||||
toggleHistory: () => {},
|
||||
$applyAsync: sinon.stub(),
|
||||
toggleHistory: sinon.stub(),
|
||||
...scope,
|
||||
}
|
||||
|
||||
const fileTreeManager = {
|
||||
findEntityById: () => null,
|
||||
findEntityByPath: () => null,
|
||||
getEntityPath: () => '',
|
||||
getRootDocDirname: () => '',
|
||||
}
|
||||
|
||||
const editorManager = {
|
||||
getCurrentDocId: () => 'foo',
|
||||
openDoc: sinon.stub(),
|
||||
}
|
||||
|
||||
window._ide = {
|
||||
$scope,
|
||||
socket,
|
||||
clsiServerId,
|
||||
editorManager,
|
||||
fileTreeManager,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue