mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #7034 from overleaf/ta-pdf-detach-full
PDF Detach v2 GitOrigin-RevId: 3deb76474185f9176cde23ab32ef51b90df6e8e9
This commit is contained in:
parent
4d18dcb377
commit
3c01402bbd
63 changed files with 1636 additions and 891 deletions
|
@ -948,11 +948,16 @@ const ProjectController = {
|
|||
!Features.hasFeature('saas') ||
|
||||
(user.features && user.features.symbolPalette)
|
||||
|
||||
res.render('project/editor', {
|
||||
const template =
|
||||
detachRole === 'detached'
|
||||
? 'project/editor_detached'
|
||||
: 'project/editor'
|
||||
res.render(template, {
|
||||
title: project.name,
|
||||
priority_title: true,
|
||||
bodyClasses: ['editor'],
|
||||
project_id: project._id,
|
||||
projectName: project.name,
|
||||
user: {
|
||||
id: userId,
|
||||
email: user.email,
|
||||
|
|
|
@ -65,11 +65,7 @@ block content
|
|||
span.sr-only #{translate("close")}
|
||||
.system-message-content(ng-bind-html="htmlContent")
|
||||
|
||||
if detachRole === 'detached'
|
||||
div.full-size
|
||||
pdf-preview()
|
||||
else
|
||||
include ./editor/main
|
||||
include ./editor/main
|
||||
|
||||
script(type="text/ng-template", id="genericMessageModalTemplate")
|
||||
.modal-header
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
meta(name="ol-useV2History" data-type="boolean" content=useV2History)
|
||||
meta(name="ol-project_id" content=project_id)
|
||||
meta(name="ol-projectName" content=projectName)
|
||||
meta(name="ol-userSettings" data-type="json" content=userSettings)
|
||||
meta(name="ol-user" data-type="json" content=user)
|
||||
meta(name="ol-learnedWords" data-type="json" content=learnedWords)
|
||||
|
|
16
services/web/app/views/project/editor_detached.pug
Normal file
16
services/web/app/views/project/editor_detached.pug
Normal file
|
@ -0,0 +1,16 @@
|
|||
extends ../layout
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'ide-detached'
|
||||
|
||||
block vars
|
||||
- var suppressNavbar = true
|
||||
- var suppressFooter = true
|
||||
- var suppressSkipToContent = true
|
||||
- metadata.robotsNoindexNofollow = true
|
||||
|
||||
block content
|
||||
#pdf-preview-detached-root()
|
||||
|
||||
block append meta
|
||||
include ./editor/meta
|
|
@ -342,6 +342,7 @@
|
|||
"sync_project_to_github_explanation": "",
|
||||
"sync_to_dropbox": "",
|
||||
"sync_to_github": "",
|
||||
"tab_connecting": "",
|
||||
"tab_no_longer_connected": "",
|
||||
"tags": "",
|
||||
"template_approved_by_publisher": "",
|
||||
|
|
|
@ -7,7 +7,6 @@ import IconChecked from '../../../shared/components/icon-checked'
|
|||
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
||||
import IconEditorOnly from './icon-editor-only'
|
||||
import IconPdfOnly from './icon-pdf-only'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
|
||||
|
@ -59,13 +58,10 @@ function LayoutDropdownButton() {
|
|||
pdfLayout,
|
||||
} = useLayoutContext(layoutContextPropTypes)
|
||||
|
||||
const { stopCompile } = useCompileContext()
|
||||
|
||||
const handleDetach = useCallback(() => {
|
||||
detach()
|
||||
stopCompile()
|
||||
eventTracking.sendMB('project-layout-detach')
|
||||
}, [detach, stopCompile])
|
||||
}, [detach])
|
||||
|
||||
const handleReattach = useCallback(() => {
|
||||
if (detachRole !== 'detacher') {
|
||||
|
|
|
@ -1,26 +1,13 @@
|
|||
import { memo, useCallback } from 'react'
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import useDetachAction from '../../../shared/hooks/use-detach-action'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import PdfCompileButtonInner from './pdf-compile-button-inner'
|
||||
|
||||
export function DetachCompileButton() {
|
||||
const { compiling, hasChanges, startCompile } = useCompileContext()
|
||||
|
||||
const startOrTriggerCompile = useDetachAction(
|
||||
'start-compile',
|
||||
startCompile,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
|
||||
const handleStartCompile = useCallback(
|
||||
() => startOrTriggerCompile(),
|
||||
[startOrTriggerCompile]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames({
|
||||
|
@ -29,7 +16,7 @@ export function DetachCompileButton() {
|
|||
})}
|
||||
>
|
||||
<PdfCompileButtonInner
|
||||
startCompile={handleStartCompile}
|
||||
startCompile={startCompile}
|
||||
compiling={compiling}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import Icon from '../../../shared/components/icon'
|
|||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
|
||||
function PdfClearCacheButton() {
|
||||
const { compiling, clearCache, clearingCache } = useCompileContext()
|
||||
|
@ -14,7 +14,7 @@ function PdfClearCacheButton() {
|
|||
bsSize="small"
|
||||
bsStyle="danger"
|
||||
className="logs-pane-actions-clear-cache"
|
||||
onClick={clearCache}
|
||||
onClick={() => clearCache()}
|
||||
disabled={clearingCache || compiling}
|
||||
>
|
||||
{clearingCache ? <Icon type="refresh" spin /> : <Icon type="trash-o" />}
|
||||
|
|
|
@ -4,7 +4,7 @@ import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import PdfCompileButtonInner from './pdf-compile-button-inner'
|
||||
|
||||
function PdfCompileButton() {
|
||||
|
@ -84,7 +84,7 @@ function PdfCompileButton() {
|
|||
<MenuItem divider />
|
||||
|
||||
<MenuItem
|
||||
onSelect={stopCompile}
|
||||
onSelect={() => stopCompile()}
|
||||
disabled={!compiling}
|
||||
aria-disabled={!compiling}
|
||||
>
|
||||
|
@ -92,7 +92,7 @@ function PdfCompileButton() {
|
|||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onSelect={recompileFromScratch}
|
||||
onSelect={() => recompileFromScratch()}
|
||||
disabled={compiling}
|
||||
aria-disabled={compiling}
|
||||
>
|
||||
|
|
|
@ -3,7 +3,7 @@ import PdfFileList from './pdf-file-list'
|
|||
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
|
||||
function PdfDownloadFilesButton() {
|
||||
const { compiling, fileList } = useCompileContext()
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import { memo, useCallback } from 'react'
|
||||
import { sendMBOnce } from '../../../infrastructure/event-tracking'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
|
||||
function PdfHybridCodeCheckButton() {
|
||||
const { codeCheckFailed, error, setShowLogs } = useCompileContext()
|
||||
const { codeCheckFailed, error, toggleLogs } = useCompileContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setShowLogs(value => {
|
||||
if (!value) {
|
||||
sendMBOnce('ide-open-logs-once')
|
||||
}
|
||||
|
||||
return !value
|
||||
})
|
||||
}, [setShowLogs])
|
||||
toggleLogs()
|
||||
}, [toggleLogs])
|
||||
|
||||
if (!codeCheckFailed) {
|
||||
return null
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { memo } from 'react'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
|
||||
function PdfHybridDownloadButton() {
|
||||
const { pdfDownloadUrl } = useCompileContext()
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Label, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import { sendMBOnce } from '../../../infrastructure/event-tracking'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
|
||||
function PdfHybridLogsButton() {
|
||||
const { error, logEntries, setShowLogs, showLogs } = useCompileContext()
|
||||
const { error, logEntries, toggleLogs, showLogs } = useCompileContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setShowLogs(value => {
|
||||
if (!value) {
|
||||
sendMBOnce('ide-open-logs-once')
|
||||
}
|
||||
|
||||
return !value
|
||||
})
|
||||
}, [setShowLogs])
|
||||
toggleLogs()
|
||||
}, [toggleLogs])
|
||||
|
||||
const errorCount = Number(logEntries?.errors?.length)
|
||||
const warningCount = Number(logEntries?.warnings?.length)
|
||||
|
|
|
@ -8,7 +8,7 @@ import { buildHighlightElement } from '../util/highlights'
|
|||
import PDFJSWrapper from '../util/pdf-js-wrapper'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import ErrorBoundaryFallback from './error-boundary-fallback'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import getMeta from '../../../utils/meta'
|
||||
|
||||
function PdfJsViewer({ url }) {
|
||||
|
|
|
@ -3,8 +3,10 @@ import classNames from 'classnames'
|
|||
import { memo, useCallback } from 'react'
|
||||
import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-header'
|
||||
import PdfLogEntryContent from './pdf-log-entry-content'
|
||||
import HumanReadableLogsHints from '../../../ide/human-readable-logs/HumanReadableLogsHints'
|
||||
|
||||
function PdfLogEntry({
|
||||
ruleId,
|
||||
headerTitle,
|
||||
headerIcon,
|
||||
rawContent,
|
||||
|
@ -20,6 +22,12 @@ function PdfLogEntry({
|
|||
onSourceLocationClick,
|
||||
onClose,
|
||||
}) {
|
||||
if (ruleId && HumanReadableLogsHints[ruleId]) {
|
||||
const hint = HumanReadableLogsHints[ruleId]
|
||||
formattedContent = hint.formattedContent
|
||||
extraInfoURL = hint.extraInfoURL
|
||||
}
|
||||
|
||||
const handleLogEntryLinkClick = useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
|
@ -56,6 +64,7 @@ function PdfLogEntry({
|
|||
}
|
||||
|
||||
PdfLogEntry.propTypes = {
|
||||
ruleId: PropTypes.string,
|
||||
sourceLocation: PreviewLogEntryHeader.propTypes.sourceLocation,
|
||||
headerTitle: PropTypes.string,
|
||||
headerIcon: PropTypes.element,
|
||||
|
|
|
@ -48,11 +48,10 @@ function PdfLogsEntries({ entries, hasErrors }) {
|
|||
{logEntries.map(logEntry => (
|
||||
<PdfLogEntry
|
||||
key={logEntry.key}
|
||||
ruleId={logEntry.ruleId}
|
||||
headerTitle={logEntry.message}
|
||||
rawContent={logEntry.content}
|
||||
logType={logEntry.type}
|
||||
formattedContent={logEntry.humanReadableHintComponent}
|
||||
extraInfoURL={logEntry.extraInfoURL}
|
||||
level={logEntry.level}
|
||||
entryAriaLabel={t('log_entry_description', {
|
||||
level: logEntry.level,
|
||||
|
|
|
@ -11,7 +11,7 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
|
|||
import ErrorBoundaryFallback from './error-boundary-fallback'
|
||||
import PdfCodeCheckFailedNotice from './pdf-code-check-failed-notice'
|
||||
import PdfLogsPaneInfoNotice from './pdf-logs-pane-info-notice'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import PdfLogEntry from './pdf-log-entry'
|
||||
|
||||
function PdfLogsViewer() {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import ReactDOM from 'react-dom'
|
||||
import PdfPreview from './pdf-preview'
|
||||
import { ContextRoot } from '../../../shared/context/root-context'
|
||||
|
||||
function PdfPreviewDetachedRoot() {
|
||||
return (
|
||||
<ContextRoot>
|
||||
<PdfPreview />
|
||||
</ContextRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export default PdfPreviewDetachedRoot // for testing
|
||||
|
||||
const element = document.getElementById('pdf-preview-detached-root')
|
||||
if (element) {
|
||||
ReactDOM.render(<PdfPreviewDetachedRoot />, element)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { memo } from 'react'
|
||||
import { memo, useState, useEffect, useRef } from 'react'
|
||||
import { ButtonToolbar } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
|
@ -9,19 +9,46 @@ import PdfHybridDownloadButton from './pdf-hybrid-download-button'
|
|||
import PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
|
||||
import PdfOrphanRefreshButton from './pdf-orphan-refresh-button'
|
||||
import { DetachedSynctexControl } from './detach-synctex-control'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
const ORPHAN_UI_TIMEOUT_MS = 5000
|
||||
|
||||
function PdfPreviewHybridToolbar() {
|
||||
const { detachRole, detachIsLinked } = useLayoutContext()
|
||||
|
||||
const uiTimeoutRef = useRef()
|
||||
const [orphanPdfTabAfterDelay, setOrphanPdfTabAfterDelay] = useState(false)
|
||||
|
||||
const orphanPdfTab = !detachIsLinked && detachRole === 'detached'
|
||||
|
||||
useEffect(() => {
|
||||
if (uiTimeoutRef.current) {
|
||||
clearTimeout(uiTimeoutRef.current)
|
||||
}
|
||||
|
||||
if (orphanPdfTab) {
|
||||
uiTimeoutRef.current = setTimeout(() => {
|
||||
setOrphanPdfTabAfterDelay(true)
|
||||
}, ORPHAN_UI_TIMEOUT_MS)
|
||||
} else {
|
||||
setOrphanPdfTabAfterDelay(false)
|
||||
}
|
||||
}, [orphanPdfTab])
|
||||
|
||||
let ToolbarInner = null
|
||||
if (orphanPdfTabAfterDelay) {
|
||||
// when the detached tab has been orphan for a while
|
||||
ToolbarInner = <PdfPreviewHybridToolbarOrphanInner />
|
||||
} else if (orphanPdfTab) {
|
||||
ToolbarInner = <PdfPreviewHybridToolbarConnectingInner />
|
||||
} else {
|
||||
// tab is not detached or not orphan
|
||||
ToolbarInner = <PdfPreviewHybridToolbarInner />
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
|
||||
{orphanPdfTab ? (
|
||||
<PdfPreviewHybridToolbarOrphanInner />
|
||||
) : (
|
||||
<PdfPreviewHybridToolbarInner />
|
||||
)}
|
||||
{ToolbarInner}
|
||||
</ButtonToolbar>
|
||||
)
|
||||
}
|
||||
|
@ -55,4 +82,16 @@ function PdfPreviewHybridToolbarOrphanInner() {
|
|||
)
|
||||
}
|
||||
|
||||
function PdfPreviewHybridToolbarConnectingInner() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className="toolbar-pdf-orphan">
|
||||
<Icon type="refresh" fw spin />
|
||||
{t('tab_connecting')}…
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PdfPreviewHybridToolbar)
|
||||
|
|
|
@ -4,7 +4,7 @@ import PdfLogsViewer from './pdf-logs-viewer'
|
|||
import PdfViewer from './pdf-viewer'
|
||||
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
||||
import PdfHybridPreviewToolbar from './pdf-preview-hybrid-toolbar'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
|
||||
function PdfPreviewPane() {
|
||||
const { pdfUrl } = useCompileContext()
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import classNames from 'classnames'
|
||||
import { memo, useCallback, useEffect, useState, useMemo } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
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 { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
|
@ -134,32 +134,19 @@ function PdfSynctexControls() {
|
|||
|
||||
const { signal } = useAbortController()
|
||||
|
||||
// for detacher editor tab, which cannot access pdfUrl in a scope value in
|
||||
// detached state
|
||||
const [pdfExists, setPdfExists] = useDetachState(
|
||||
'pdf-exists',
|
||||
!!pdfUrl,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setPdfExists(!!pdfUrl)
|
||||
}, [pdfUrl, setPdfExists])
|
||||
|
||||
useEffect(() => {
|
||||
const listener = event => setCursorPosition(event.detail)
|
||||
window.addEventListener('cursor:editor:update', listener)
|
||||
return () => window.removeEventListener('cursor:editor:update', listener)
|
||||
}, [ide])
|
||||
|
||||
const [syncToPdfInFlight, setSyncToPdfInFlight] = useDetachState(
|
||||
'sync-to-pdf-inflight',
|
||||
const [syncToPdfInFlight, setSyncToPdfInFlight] = useState(false)
|
||||
const [syncToCodeInFlight, setSyncToCodeInFlight] = useDetachState(
|
||||
'sync-to-code-inflight',
|
||||
false,
|
||||
'detached',
|
||||
'detacher'
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [syncToCodeInFlight, setSyncToCodeInFlight] = useState(false)
|
||||
|
||||
const [, setSynctexError] = useScopeValue('sync_tex_error')
|
||||
|
||||
|
@ -179,7 +166,7 @@ function PdfSynctexControls() {
|
|||
return path
|
||||
}, [ide])
|
||||
|
||||
const _goToCodeLine = useCallback(
|
||||
const goToCodeLine = useCallback(
|
||||
(file, line) => {
|
||||
if (file) {
|
||||
const doc = ide.fileTreeManager.findEntityByPath(file)
|
||||
|
@ -200,14 +187,7 @@ function PdfSynctexControls() {
|
|||
[ide, isMounted, setSynctexError]
|
||||
)
|
||||
|
||||
const goToCodeLine = useDetachAction(
|
||||
'go-to-code-line',
|
||||
_goToCodeLine,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
|
||||
const _goToPdfLocation = useCallback(
|
||||
const goToPdfLocation = useCallback(
|
||||
params => {
|
||||
setSyncToPdfInFlight(true)
|
||||
|
||||
|
@ -240,13 +220,6 @@ function PdfSynctexControls() {
|
|||
]
|
||||
)
|
||||
|
||||
const goToPdfLocation = useDetachAction(
|
||||
'go-to-pdf-location',
|
||||
_goToPdfLocation,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
|
||||
const syncToPdf = useCallback(
|
||||
cursorPosition => {
|
||||
const params = new URLSearchParams({
|
||||
|
@ -260,7 +233,7 @@ function PdfSynctexControls() {
|
|||
[getCurrentFilePath, goToPdfLocation]
|
||||
)
|
||||
|
||||
const syncToCode = useCallback(
|
||||
const _syncToCode = useCallback(
|
||||
(position, visualOffset = 0) => {
|
||||
setSyncToCodeInFlight(true)
|
||||
// FIXME: this actually works better if it's halfway across the
|
||||
|
@ -317,6 +290,13 @@ function PdfSynctexControls() {
|
|||
]
|
||||
)
|
||||
|
||||
const syncToCode = useDetachAction(
|
||||
'sync-to-code',
|
||||
_syncToCode,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const listener = event => syncToCode(event.detail)
|
||||
window.addEventListener('synctex:sync-to-position', listener)
|
||||
|
@ -325,22 +305,32 @@ function PdfSynctexControls() {
|
|||
}
|
||||
}, [syncToCode])
|
||||
|
||||
const hasSingleSelectedDoc = useMemo(() => {
|
||||
const [hasSingleSelectedDoc, setHasSingleSelectedDoc] = useDetachState(
|
||||
'has-single-selected-doc',
|
||||
false,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEntities.length !== 1) {
|
||||
return false
|
||||
setHasSingleSelectedDoc(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedEntities[0].type !== 'doc') {
|
||||
return false
|
||||
setHasSingleSelectedDoc(false)
|
||||
return
|
||||
}
|
||||
return true
|
||||
}, [selectedEntities])
|
||||
|
||||
setHasSingleSelectedDoc(true)
|
||||
}, [selectedEntities, setHasSingleSelectedDoc])
|
||||
|
||||
if (!position) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!pdfExists || pdfViewer === 'native') {
|
||||
if (!pdfUrl || pdfViewer === 'native') {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { lazy, memo } from 'react'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
|
||||
const PdfJsViewer = lazy(() =>
|
||||
import(/* webpackChunkName: "pdf-js-viewer" */ './pdf-js-viewer')
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||
import { useDetachContext } from '../../../shared/context/detach-context'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import useEventListener from '../../../shared/hooks/use-event-listener'
|
||||
import useDetachAction from '../../../shared/hooks/use-detach-action'
|
||||
import usePreviousValue from '../../../shared/hooks/use-previous-value'
|
||||
|
||||
const showPdfDetach = getMeta('ol-showPdfDetach')
|
||||
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
||||
|
||||
export default function useCompileTriggers() {
|
||||
const { startCompile, setChangedAt, cleanupCompileResult, setError } =
|
||||
useCompileContext()
|
||||
const { role: detachRole } = useDetachContext()
|
||||
const { startCompile, setChangedAt } = useCompileContext()
|
||||
|
||||
// recompile on key press
|
||||
const startOrTriggerCompile = useDetachAction(
|
||||
|
@ -43,23 +38,4 @@ export default function useCompileTriggers() {
|
|||
}, [setOrTriggerChangedAt, setChangedAt])
|
||||
useEventListener('doc:changed', setChangedAtHandler)
|
||||
useEventListener('doc:saved', setChangedAtHandler)
|
||||
|
||||
// clear preview and recompile when the detach role is reset
|
||||
const previousDetachRole = usePreviousValue(detachRole)
|
||||
useEffect(() => {
|
||||
if (previousDetachRole && !detachRole) {
|
||||
if (debugPdfDetach) {
|
||||
console.log('Recompile on reattach', { previousDetachRole, detachRole })
|
||||
}
|
||||
cleanupCompileResult()
|
||||
setError()
|
||||
startCompile()
|
||||
}
|
||||
}, [
|
||||
cleanupCompileResult,
|
||||
setError,
|
||||
startCompile,
|
||||
previousDetachRole,
|
||||
detachRole,
|
||||
])
|
||||
}
|
||||
|
|
5
services/web/frontend/js/ide-detached.js
Normal file
5
services/web/frontend/js/ide-detached.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import './utils/meta'
|
||||
import './utils/webpack-public-path'
|
||||
import './infrastructure/error-reporter'
|
||||
import './i18n'
|
||||
import './features/pdf-preview/components/pdf-preview-detached-root'
|
|
@ -28,11 +28,6 @@ export default {
|
|||
let type
|
||||
if (ruleDetails.ruleId != null) {
|
||||
entry.ruleId = ruleDetails.ruleId
|
||||
} else if (ruleDetails.regexToMatch != null) {
|
||||
entry.ruleId = `hint_${ruleDetails.regexToMatch
|
||||
.toString()
|
||||
.replace(/\s/g, '_')
|
||||
.slice(1, -1)}`
|
||||
}
|
||||
if (ruleDetails.newMessage != null) {
|
||||
entry.message = entry.message.replace(
|
||||
|
@ -54,19 +49,6 @@ export default {
|
|||
seenErrorTypes[type] = true
|
||||
}
|
||||
}
|
||||
|
||||
if (ruleDetails.humanReadableHint != null) {
|
||||
entry.humanReadableHint = ruleDetails.humanReadableHint
|
||||
}
|
||||
|
||||
if (ruleDetails.humanReadableHintComponent != null) {
|
||||
entry.humanReadableHintComponent =
|
||||
ruleDetails.humanReadableHintComponent
|
||||
}
|
||||
|
||||
if (ruleDetails.extraInfoURL != null) {
|
||||
entry.extraInfoURL = ruleDetails.extraInfoURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,449 @@
|
|||
import PropTypes from 'prop-types'
|
||||
|
||||
function WikiLink({ url, children }) {
|
||||
if (window.wikiEnabled) {
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return <>{children}</>
|
||||
}
|
||||
}
|
||||
|
||||
WikiLink.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
const hints = {
|
||||
hint_misplaced_alignment_tab_character: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Misplaced_alignment_tab_character_%26',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have placed an alignment tab character '&' in the wrong place. If
|
||||
you want to align something, you must write it inside an align
|
||||
environment such as \begin
|
||||
{'{align}'} … \end
|
||||
{'{align}'}, \begin
|
||||
{'{tabular}'} … \end
|
||||
{'{tabular}'}, etc. If you want to write an ampersand '&' in text, you
|
||||
must write \& instead.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_extra_alignment_tab_has_been_changed: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have written too many alignment tabs in a table, causing one of them
|
||||
to be turned into a line break. Make sure you have specified the correct
|
||||
number of columns in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Tables">table</WikiLink>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_display_math_should_end_with: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Display_math_should_end_with_$$',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have forgotten a $ sign at the end of 'display math' mode. When
|
||||
writing in display math mode, you must always math write inside $$ … $$.
|
||||
Check that the number of $s match around each math expression.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_missing_inserted: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Missing_$_inserted',
|
||||
formattedContent: (
|
||||
<>
|
||||
<p>
|
||||
You need to enclose all mathematical expressions and symbols with
|
||||
special markers. These special markers create a ‘math mode’.
|
||||
</p>
|
||||
<p>
|
||||
Use <code>$...$</code> for inline math mode, and <code>\[...\]</code>
|
||||
or one of the mathematical environments (e.g. equation) for display
|
||||
math mode.
|
||||
</p>
|
||||
<p>
|
||||
This applies to symbols such as subscripts ( <code>_</code> ),
|
||||
integrals ( <code>\int</code> ), Greek letters ( <code>\alpha</code>,{' '}
|
||||
<code>\beta</code>, <code>\delta</code> ) and modifiers{' '}
|
||||
<code>{'(\\vec{x}'}</code>, <code>{'\\tilde{x}'})</code>.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_reference_undefined: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_undefined_references',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have referenced something which has not yet been labelled. If you
|
||||
have labelled it already, make sure that what is written inside \ref
|
||||
{'{...}'} is the same as what is written inside \label
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_there_were_undefined_references: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_undefined_references',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have referenced something which has not yet been labelled. If you
|
||||
have labelled it already, make sure that what is written inside \ref
|
||||
{'{...}'} is the same as what is written inside \label
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_citation_on_page_undefined_on_input_line: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have cited something which is not included in your bibliography.
|
||||
Make sure that the citation (\cite
|
||||
{'{...}'}) has a corresponding key in your bibliography, and that both
|
||||
are spelled the same way.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_label_multiply_defined_labels: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_multiply-defined_labels',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used the same label more than once. Check that each \label
|
||||
{'{...}'} labels only one item.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_float_specifier_changed: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27',
|
||||
formattedContent: (
|
||||
<>
|
||||
The float specifier 'h' is too strict of a demand for LaTeX to place
|
||||
your float in a nice way here. Try relaxing it by using 'ht', or even
|
||||
'htbp' if necessary. If you want to try keep the float here anyway,
|
||||
check out the{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
float package
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_no_positions_in_optional_float_specifier: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/No_positions_in_optional_float_specifier',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have forgotten to include a float specifier, which tells LaTeX where
|
||||
to position your figure. To fix this, either insert a float specifier
|
||||
inside the square brackets (e.g. \begin
|
||||
{'{figure}'}
|
||||
[h]), or remove the square brackets (e.g. \begin
|
||||
{'{figure}'}
|
||||
). Find out more about float specifiers{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
here
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_undefined_control_sequence: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Undefined_control_sequence',
|
||||
formattedContent: (
|
||||
<>
|
||||
The compiler is having trouble understanding a command you have used.
|
||||
Check that the command is spelled correctly. If the command is part of a
|
||||
package, make sure you have included the package in your preamble using
|
||||
\usepackage
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_file_not_found: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/File_XXX_not_found_on_input_line_XXX',
|
||||
formattedContent: (
|
||||
<>
|
||||
The compiler cannot find the file you want to include. Make sure that
|
||||
you have{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Including_images_in_ShareLaTeX">
|
||||
uploaded the file
|
||||
</WikiLink>{' '}
|
||||
and{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/File_XXX_not_found_on_input_line_XXX.">
|
||||
specified the file location correctly
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_unknown_graphics_extension: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.XXX',
|
||||
formattedContent: (
|
||||
<>
|
||||
The compiler does not recognise the file type of one of your images.
|
||||
Make sure you are using a{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif.">
|
||||
supported image format
|
||||
</WikiLink>{' '}
|
||||
for your choice of compiler, and check that there are no periods (.) in
|
||||
the name of your image.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_unknown_float_option_h: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27',
|
||||
formattedContent: (
|
||||
<>
|
||||
The compiler isn't recognizing the float option 'H'. Include \usepackage
|
||||
{'{float}'} in your preamble to fix this.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_unknown_float_option_q: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60q%27',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used a float specifier which the compiler does not understand.
|
||||
You can learn more about the different float options available for
|
||||
placing figures{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
here
|
||||
</WikiLink>{' '}
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_math_allowed_only_in_math_mode: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used a font command which is only available in math mode. To
|
||||
use this command, you must be in maths mode (E.g. $ … $ or \begin
|
||||
{'{math}'} … \end
|
||||
{'{math}'}
|
||||
). If you want to use it outside of math mode, use the text version
|
||||
instead: \textrm, \textit, etc.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment: {
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{...}'} without a corresponding \end
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_brackets: {
|
||||
formattedContent: (
|
||||
<>You have used an open bracket without a corresponding close bracket.</>
|
||||
),
|
||||
},
|
||||
hint_can_be_used_only_in_preamble: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Can_be_used_only_in_preamble',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used a command in the main body of your document which should
|
||||
be used in the preamble. Make sure that \documentclass[…]
|
||||
{'{…}'} and all \usepackage
|
||||
{'{…}'} commands are written before \begin
|
||||
{'{document}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_missing_right_inserted: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Missing_%5Cright_insertede',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have started an expression with a \left command, but have not
|
||||
included a corresponding \right command. Make sure that your \left and
|
||||
\right commands balance everywhere, or else try using \Biggl and \Biggr
|
||||
commands instead as shown{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/Missing_%5Cright_inserted">
|
||||
here
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_double_superscript: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Double_superscript',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have written a double superscript incorrectly as a^b^c, or else you
|
||||
have written a prime with a superscript. Remember to include {'{and}'}{' '}
|
||||
when using multiple superscripts. Try a^
|
||||
{'{b ^ c}'} instead.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_double_subscript: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Double_subscript',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have written a double subscript incorrectly as a_b_c. Remember to
|
||||
include {'{and}'} when using multiple subscripts. Try a_
|
||||
{'{b_c}'} instead.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_no_author_given: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/No_%5Cauthor_given',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used the \maketitle command, but have not specified any
|
||||
\author. To fix this, include an author in your preamble using the
|
||||
\author
|
||||
{'{…}'} command.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_environment_undefined: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FLaTeX%20Error%3A%20Environment%20XXX%20undefined',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have created an environment (using \begin
|
||||
{'{…}'} and \end
|
||||
{'{…}'} commands) which is not recognized. Make sure you have included
|
||||
the required package for that environment in your preamble, and that the
|
||||
environment is spelled correctly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_somethings_wrong_perhaps_a_missing_item: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Something%27s_wrong--perhaps_a_missing_%5Citem',
|
||||
formattedContent: (
|
||||
<>
|
||||
There are no entries found in a list you have created. Make sure you
|
||||
label list entries using the \item command, and that you have not used a
|
||||
list inside a table.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_misplaced_noalign: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Misplaced_%5Cnoalign',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used a \hline command in the wrong place, probably outside a
|
||||
table. If the \hline command is written inside a table, try including \\
|
||||
before it.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_no_line_here_to_end: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_There%27s_no_line_here_to_end',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used a \\ or \newline command where LaTeX was not expecting
|
||||
one. Make sure that you only use line breaks after blocks of text, and
|
||||
be careful using linebreaks inside lists and other environments.\
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_verb_ended_by_end_of_line: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_%5Cverb_ended_by_end_of_line',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used a \verb command incorrectly. Try replacling the \verb
|
||||
command with \begin
|
||||
{'{verbatim}'}
|
||||
…\end
|
||||
{'{verbatim}'}
|
||||
.\
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_illegal_unit_of_measure_pt_inserted: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FIllegal%20unit%20of%20measure%20(pt%20inserted)',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have written a length, but have not specified the appropriate units
|
||||
(pt, mm, cm etc.). If you have not written a length, check that you have
|
||||
not witten a linebreak \\ followed by square brackets […] anywhere.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_extra_right: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Extra_%5Cright',
|
||||
formattedContent: (
|
||||
<>
|
||||
You have written a \right command without a corresponding \left command.
|
||||
Check that all \left and \right commands balance everywhere.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_missing_begin_document_: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FLaTeX%20Error%3A%20Missing%20%5Cbegin%20document',
|
||||
formattedContent: (
|
||||
<>
|
||||
No \begin
|
||||
{'{document}'} command was found. Make sure you have included \begin
|
||||
{'{document}'} in your preamble, and that your main document is set
|
||||
correctly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment2: {
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment3: {
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment4: {
|
||||
formattedContent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
if (!window.wikiEnabled) {
|
||||
Object.keys(hints).forEach(ruleId => {
|
||||
hints[ruleId].extraInfoURL = null
|
||||
})
|
||||
}
|
||||
|
||||
export default hints
|
|
@ -1,515 +1,133 @@
|
|||
/* eslint-disable no-useless-escape */
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
function WikiLink({ url, children }) {
|
||||
if (window.wikiEnabled) {
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return <>{children}</>
|
||||
}
|
||||
}
|
||||
|
||||
WikiLink.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
const rules = [
|
||||
{
|
||||
ruleId: 'hint_misplaced_alignment_tab_character',
|
||||
regexToMatch: /Misplaced alignment tab character \&/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Misplaced_alignment_tab_character_%26',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have placed an alignment tab character '&' in the wrong place. If
|
||||
you want to align something, you must write it inside an align
|
||||
environment such as \begin
|
||||
{'{align}'} … \end
|
||||
{'{align}'}, \begin
|
||||
{'{tabular}'} … \end
|
||||
{'{tabular}'}, etc. If you want to write an ampersand '&' in text, you
|
||||
must write \& instead.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have placed an alignment tab character '&' in the wrong place. If you want to align something, you must write it inside an align environment such as \\begin{align} … \\end{align}, \\begin{tabular} … \\end{tabular}, etc. If you want to write an ampersand '&' in text, you must write \\& instead.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_extra_alignment_tab_has_been_changed',
|
||||
regexToMatch: /Extra alignment tab has been changed to \\cr/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have written too many alignment tabs in a table, causing one of them
|
||||
to be turned into a line break. Make sure you have specified the correct
|
||||
number of columns in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Tables">table</WikiLink>.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have written too many alignment tabs in a table, causing one of them to be turned into a line break. Make sure you have specified the correct number of columns in your <a href="https://www.overleaf.com/learn/Tables" target="_blank">table</a>.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_display_math_should_end_with',
|
||||
regexToMatch: /Display math should end with \$\$/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Display_math_should_end_with_$$',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have forgotten a $ sign at the end of 'display math' mode. When
|
||||
writing in display math mode, you must always math write inside $$ … $$.
|
||||
Check that the number of $s match around each math expression.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have forgotten a $ sign at the end of 'display math' mode. When writing in display math mode, you must always math write inside $$ … $$. Check that the number of $s match around each math expression.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_missing_inserted',
|
||||
regexToMatch: /Missing [{$] inserted./,
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Missing_$_inserted',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
<p>
|
||||
You need to enclose all mathematical expressions and symbols with
|
||||
special markers. These special markers create a ‘math mode’.
|
||||
</p>
|
||||
<p>
|
||||
Use <code>$...$</code> for inline math mode, and <code>\[...\]</code>
|
||||
or one of the mathematical environments (e.g. equation) for display
|
||||
math mode.
|
||||
</p>
|
||||
<p>
|
||||
This applies to symbols such as subscripts ( <code>_</code> ),
|
||||
integrals ( <code>\int</code> ), Greek letters ( <code>\alpha</code>,{' '}
|
||||
<code>\beta</code>, <code>\delta</code> ) and modifiers{' '}
|
||||
<code>{'(\\vec{x}'}</code>, <code>{'\\tilde{x}'})</code>.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You need to enclose all mathematical expressions and symbols with special markers. These special markers create a ‘math mode’. Use $...$ for inline math mode, and \\[...\\] or one of the mathematical environments (e.g. equation) for display math mode. This applies to symbols such as subscripts ( _ ), integrals ( \\int ), Greek letters ( \\alpha, \\beta, \\delta ) and modifiers (\\vec{x}, \\tilde{x} ).',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_reference_undefined',
|
||||
regexToMatch: /Reference.+undefined/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_undefined_references',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have referenced something which has not yet been labelled. If you
|
||||
have labelled it already, make sure that what is written inside \ref
|
||||
{'{...}'} is the same as what is written inside \label
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have referenced something which has not yet been labelled. If you have labelled it already, make sure that what is written inside \\ref{...} is the same as what is written inside \\label{...}.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_there_were_undefined_references',
|
||||
regexToMatch: /There were undefined references/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_undefined_references',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have referenced something which has not yet been labelled. If you
|
||||
have labelled it already, make sure that what is written inside \ref
|
||||
{'{...}'} is the same as what is written inside \label
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have referenced something which has not yet been labelled. If you have labelled it already, make sure that what is written inside \\ref{...} is the same as what is written inside \\label{...}.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_citation_on_page_undefined_on_input_line',
|
||||
regexToMatch: /Citation .+ on page .+ undefined on input line .+/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have cited something which is not included in your bibliography.
|
||||
Make sure that the citation (\cite
|
||||
{'{...}'}) has a corresponding key in your bibliography, and that both
|
||||
are spelled the same way.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have cited something which is not included in your bibliography. Make sure that the citation (\\cite{...}) has a corresponding key in your bibliography, and that both are spelled the same way.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_label_multiply_defined_labels',
|
||||
regexToMatch: /(Label .+)? multiply[ -]defined( labels)?/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_multiply-defined_labels',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used the same label more than once. Check that each \label
|
||||
{'{...}'} labels only one item.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used the same label more than once. Check that each \\label{...} labels only one item.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_float_specifier_changed',
|
||||
regexToMatch: /`!?h' float specifier changed to `!?ht'/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
The float specifier 'h' is too strict of a demand for LaTeX to place
|
||||
your float in a nice way here. Try relaxing it by using 'ht', or even
|
||||
'htbp' if necessary. If you want to try keep the float here anyway,
|
||||
check out the{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
float package
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'The float specifier 'h' is too strict of a demand for LaTeX to place your float in a nice way here. Try relaxing it by using 'ht', or even 'htbp' if necessary. If you want to try keep the float here anyway, check out the <a href="https://www.overleaf.com/learn/Positioning_of_Figures" target="_blank">float package</a>.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_no_positions_in_optional_float_specifier',
|
||||
regexToMatch: /No positions in optional float specifier/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/No_positions_in_optional_float_specifier',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have forgotten to include a float specifier, which tells LaTeX where
|
||||
to position your figure. To fix this, either insert a float specifier
|
||||
inside the square brackets (e.g. \begin
|
||||
{'{figure}'}
|
||||
[h]), or remove the square brackets (e.g. \begin
|
||||
{'{figure}'}
|
||||
). Find out more about float specifiers{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
here
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have forgotten to include a float specifier, which tells LaTeX where to position your figure. To fix this, either insert a float specifier inside the square brackets (e.g. \\begin{figure}[h]), or remove the square brackets (e.g. \\begin{figure}). Find out more about float specifiers <a href="https://www.overleaf.com/learn/Positioning_of_Figures" target="_blank">here</a>.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_undefined_control_sequence',
|
||||
regexToMatch: /Undefined control sequence/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Undefined_control_sequence',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
The compiler is having trouble understanding a command you have used.
|
||||
Check that the command is spelled correctly. If the command is part of a
|
||||
package, make sure you have included the package in your preamble using
|
||||
\usepackage
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \\usepackage{...}.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_file_not_found',
|
||||
regexToMatch: /File .+ not found/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/File_XXX_not_found_on_input_line_XXX',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
The compiler cannot find the file you want to include. Make sure that
|
||||
you have{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Including_images_in_ShareLaTeX">
|
||||
uploaded the file
|
||||
</WikiLink>{' '}
|
||||
and{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/File_XXX_not_found_on_input_line_XXX.">
|
||||
specified the file location correctly
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'The compiler cannot find the file you want to include. Make sure that you have <a href="https://www.overleaf.com/learn/Including_images_in_ShareLaTeX" target="_blank">uploaded the file</a> and <a href="https://www.overleaf.com/learn/Errors/File_XXX_not_found_on_input_line_XXX." target="_blank">specified the file location correctly</a>.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_unknown_graphics_extension',
|
||||
regexToMatch: /LaTeX Error: Unknown graphics extension: \..+/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.XXX',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
The compiler does not recognise the file type of one of your images.
|
||||
Make sure you are using a{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif.">
|
||||
supported image format
|
||||
</WikiLink>{' '}
|
||||
for your choice of compiler, and check that there are no periods (.) in
|
||||
the name of your image.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'The compiler does not recognise the file type of one of your images. Make sure you are using a <a href="https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif." target="_blank">supported image format</a> for your choice of compiler, and check that there are no periods (.) in the name of your image.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_unknown_float_option_h',
|
||||
regexToMatch: /LaTeX Error: Unknown float option `H'/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
The compiler isn't recognizing the float option 'H'. Include \usepackage
|
||||
{'{float}'} in your preamble to fix this.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'The compiler isn't recognizing the float option 'H'. Include \\usepackage{float} in your preamble to fix this.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_unknown_float_option_q',
|
||||
regexToMatch: /LaTeX Error: Unknown float option `q'/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60q%27',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used a float specifier which the compiler does not understand.
|
||||
You can learn more about the different float options available for
|
||||
placing figures{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
here
|
||||
</WikiLink>{' '}
|
||||
.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used a float specifier which the compiler does not understand. You can learn more about the different float options available for placing figures <a href="https://www.overleaf.com/learn/Positioning_of_Figures" target="_blank">here</a> .',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_math_allowed_only_in_math_mode',
|
||||
regexToMatch: /LaTeX Error: \\math.+ allowed only in math mode/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used a font command which is only available in math mode. To
|
||||
use this command, you must be in maths mode (E.g. $ … $ or \begin
|
||||
{'{math}'} … \end
|
||||
{'{math}'}
|
||||
). If you want to use it outside of math mode, use the text version
|
||||
instead: \textrm, \textit, etc.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ … $ or \\begin{math} … \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_mismatched_environment',
|
||||
types: ['environment'],
|
||||
regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/,
|
||||
newMessage: 'Error: environment does not match \\begin{$1} ... \\end{$2}',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{...}'} without a corresponding \end
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used \\begin{...} without a corresponding \\end{...}.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_mismatched_brackets',
|
||||
types: ['environment'],
|
||||
regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/,
|
||||
newMessage: "Error: brackets do not match, found '$2' instead of '$1'",
|
||||
humanReadableHintComponent: (
|
||||
<>You have used an open bracket without a corresponding close bracket.</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used an open bracket without a corresponding close bracket.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_can_be_used_only_in_preamble',
|
||||
regexToMatch: /LaTeX Error: Can be used only in preamble/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Can_be_used_only_in_preamble',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used a command in the main body of your document which should
|
||||
be used in the preamble. Make sure that \documentclass[…]
|
||||
{'{…}'} and all \usepackage
|
||||
{'{…}'} commands are written before \begin
|
||||
{'{document}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used a command in the main body of your document which should be used in the preamble. Make sure that \\documentclass[…]{…} and all \\usepackage{…} commands are written before \\begin{document}.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_missing_right_inserted',
|
||||
regexToMatch: /Missing \\right inserted/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Missing_%5Cright_insertede',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have started an expression with a \left command, but have not
|
||||
included a corresponding \right command. Make sure that your \left and
|
||||
\right commands balance everywhere, or else try using \Biggl and \Biggr
|
||||
commands instead as shown{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/Missing_%5Cright_inserted">
|
||||
here
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have started an expression with a \\left command, but have not included a corresponding \\right command. Make sure that your \\left and \\right commands balance everywhere, or else try using \\Biggl and \\Biggr commands instead as shown <a href="https://www.overleaf.com/learn/Errors/Missing_%5Cright_inserted" target="_blank">here</a>.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_double_superscript',
|
||||
regexToMatch: /Double superscript/,
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Double_superscript',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have written a double superscript incorrectly as a^b^c, or else you
|
||||
have written a prime with a superscript. Remember to include {'{and}'}{' '}
|
||||
when using multiple superscripts. Try a^
|
||||
{'{b ^ c}'} instead.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have written a double superscript incorrectly as a^b^c, or else you have written a prime with a superscript. Remember to include {and} when using multiple superscripts. Try a^{b ^ c} instead.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_double_subscript',
|
||||
regexToMatch: /Double subscript/,
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Double_subscript',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have written a double subscript incorrectly as a_b_c. Remember to
|
||||
include {'{and}'} when using multiple subscripts. Try a_
|
||||
{'{b_c}'} instead.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have written a double subscript incorrectly as a_b_c. Remember to include {and} when using multiple subscripts. Try a_{b_c} instead.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_no_author_given',
|
||||
regexToMatch: /No \\author given/,
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/No_%5Cauthor_given',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used the \maketitle command, but have not specified any
|
||||
\author. To fix this, include an author in your preamble using the
|
||||
\author
|
||||
{'{…}'} command.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used the \\maketitle command, but have not specified any \\author. To fix this, include an author in your preamble using the \\author{…} command.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_environment_undefined',
|
||||
regexToMatch: /LaTeX Error: Environment .+ undefined/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FLaTeX%20Error%3A%20Environment%20XXX%20undefined',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have created an environment (using \begin
|
||||
{'{…}'} and \end
|
||||
{'{…}'} commands) which is not recognized. Make sure you have included
|
||||
the required package for that environment in your preamble, and that the
|
||||
environment is spelled correctly.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have created an environment (using \\begin{…} and \\end{…} commands) which is not recognized. Make sure you have included the required package for that environment in your preamble, and that the environment is spelled correctly.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_somethings_wrong_perhaps_a_missing_item',
|
||||
regexToMatch: /LaTeX Error: Something's wrong--perhaps a missing \\item/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Something%27s_wrong--perhaps_a_missing_%5Citem',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
There are no entries found in a list you have created. Make sure you
|
||||
label list entries using the \item command, and that you have not used a
|
||||
list inside a table.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'There are no entries found in a list you have created. Make sure you label list entries using the \\item command, and that you have not used a list inside a table.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_misplaced_noalign',
|
||||
regexToMatch: /Misplaced \\noalign/,
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Misplaced_%5Cnoalign',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used a \hline command in the wrong place, probably outside a
|
||||
table. If the \hline command is written inside a table, try including \\
|
||||
before it.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used a \\hline command in the wrong place, probably outside a table. If the \\hline command is written inside a table, try including \\\\ before it.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_no_line_here_to_end',
|
||||
regexToMatch: /LaTeX Error: There's no line here to end/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_There%27s_no_line_here_to_end',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used a \\ or \newline command where LaTeX was not expecting
|
||||
one. Make sure that you only use line breaks after blocks of text, and
|
||||
be careful using linebreaks inside lists and other environments.\
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used a \\\\ or \\newline command where LaTeX was not expecting one. Make sure that you only use line breaks after blocks of text, and be careful using linebreaks inside lists and other environments.\\',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_verb_ended_by_end_of_line',
|
||||
regexToMatch: /LaTeX Error: \\verb ended by end of line/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_%5Cverb_ended_by_end_of_line',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used a \verb command incorrectly. Try replacling the \verb
|
||||
command with \begin
|
||||
{'{verbatim}'}
|
||||
…\end
|
||||
{'{verbatim}'}
|
||||
.\
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used a \\verb command incorrectly. Try replacling the \\verb command with \\begin{verbatim}…\\end{verbatim}.\\',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_illegal_unit_of_measure_pt_inserted',
|
||||
regexToMatch: /Illegal unit of measure (pt inserted)/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FIllegal%20unit%20of%20measure%20(pt%20inserted)',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have written a length, but have not specified the appropriate units
|
||||
(pt, mm, cm etc.). If you have not written a length, check that you have
|
||||
not witten a linebreak \\ followed by square brackets […] anywhere.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have written a length, but have not specified the appropriate units (pt, mm, cm etc.). If you have not written a length, check that you have not witten a linebreak \\\\ followed by square brackets […] anywhere.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_extra_right',
|
||||
regexToMatch: /Extra \\right/,
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Extra_%5Cright',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have written a \right command without a corresponding \left command.
|
||||
Check that all \left and \right commands balance everywhere.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have written a \\right command without a corresponding \\left command. Check that all \\left and \\right commands balance everywhere.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_missing_begin_document_',
|
||||
regexToMatch: /Missing \\begin{document}/,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FLaTeX%20Error%3A%20Missing%20%5Cbegin%20document',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
No \begin
|
||||
{'{document}'} command was found. Make sure you have included \begin
|
||||
{'{document}'} in your preamble, and that your main document is set
|
||||
correctly.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'No \\begin{document} command was found. Make sure you have included \\begin{document} in your preamble, and that your main document is set correctly.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_mismatched_environment2',
|
||||
|
@ -518,15 +136,6 @@ const rules = [
|
|||
regexToMatch:
|
||||
/Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/,
|
||||
newMessage: 'Error: environments do not match: \\begin{$1} ... \\end{$2}',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used \\begin{} without a corresponding \\end{}.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_mismatched_environment3',
|
||||
|
@ -535,15 +144,6 @@ const rules = [
|
|||
regexToMatch:
|
||||
/Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/,
|
||||
newMessage: 'Warning: No matching \\end found for \\begin{$1}',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used \\begin{} without a corresponding \\end{}.',
|
||||
},
|
||||
{
|
||||
ruleId: 'hint_mismatched_environment4',
|
||||
|
@ -552,29 +152,7 @@ const rules = [
|
|||
regexToMatch:
|
||||
/Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/,
|
||||
newMessage: 'Error: found \\end{$1} without a corresponding \\begin{$1}',
|
||||
humanReadableHintComponent: (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
humanReadableHint:
|
||||
'You have used \\begin{} without a corresponding \\end{}.',
|
||||
},
|
||||
]
|
||||
|
||||
if (!window.wikiEnabled) {
|
||||
rules.forEach(rule => {
|
||||
rule.extraInfoURL = null
|
||||
rule.humanReadableHint = stripHTMLFromString(rule.humanReadableHint)
|
||||
})
|
||||
}
|
||||
|
||||
function stripHTMLFromString(htmlStr) {
|
||||
const tmp = document.createElement('DIV')
|
||||
tmp.innerHTML = htmlStr
|
||||
return tmp.textContent || tmp.innerText || ''
|
||||
}
|
||||
|
||||
export default rules
|
||||
|
|
|
@ -0,0 +1,386 @@
|
|||
import { createContext, useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
useLocalCompileContext,
|
||||
CompileContextPropTypes,
|
||||
} from './local-compile-context'
|
||||
import useDetachStateWatcher from '../hooks/use-detach-state-watcher'
|
||||
import useDetachAction from '../hooks/use-detach-action'
|
||||
|
||||
export const DetachCompileContext = createContext()
|
||||
|
||||
DetachCompileContext.Provider.propTypes = CompileContextPropTypes
|
||||
|
||||
export function DetachCompileProvider({ children }) {
|
||||
const localCompileContext = useLocalCompileContext()
|
||||
if (!localCompileContext) {
|
||||
throw new Error(
|
||||
'DetachCompileProvider is only available inside LocalCompileProvider'
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
autoCompile: _autoCompile,
|
||||
clearingCache: _clearingCache,
|
||||
clsiServerId: _clsiServerId,
|
||||
codeCheckFailed: _codeCheckFailed,
|
||||
compiling: _compiling,
|
||||
draft: _draft,
|
||||
error: _error,
|
||||
fileList: _fileList,
|
||||
hasChanges: _hasChanges,
|
||||
highlights: _highlights,
|
||||
logEntries: _logEntries,
|
||||
logEntryAnnotations: _logEntryAnnotations,
|
||||
pdfDownloadUrl: _pdfDownloadUrl,
|
||||
pdfUrl: _pdfUrl,
|
||||
pdfViewer: _pdfViewer,
|
||||
position: _position,
|
||||
rawLog: _rawLog,
|
||||
setAutoCompile: _setAutoCompile,
|
||||
setDraft: _setDraft,
|
||||
setError: _setError,
|
||||
setHasLintingError: _setHasLintingError,
|
||||
setHighlights: _setHighlights,
|
||||
setPosition: _setPosition,
|
||||
setShowLogs: _setShowLogs,
|
||||
toggleLogs: _toggleLogs,
|
||||
setStopOnValidationError: _setStopOnValidationError,
|
||||
showLogs: _showLogs,
|
||||
stopOnValidationError: _stopOnValidationError,
|
||||
uncompiled: _uncompiled,
|
||||
validationIssues: _validationIssues,
|
||||
firstRenderDone: _firstRenderDone,
|
||||
cleanupCompileResult: _cleanupCompileResult,
|
||||
recompileFromScratch: _recompileFromScratch,
|
||||
setCompiling: _setCompiling,
|
||||
startCompile: _startCompile,
|
||||
stopCompile: _stopCompile,
|
||||
setChangedAt: _setChangedAt,
|
||||
clearCache: _clearCache,
|
||||
} = localCompileContext
|
||||
|
||||
const [autoCompile] = useDetachStateWatcher(
|
||||
'autoCompile',
|
||||
_autoCompile,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [clearingCache] = useDetachStateWatcher(
|
||||
'clearingCache',
|
||||
_clearingCache,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [clsiServerId] = useDetachStateWatcher(
|
||||
'clsiServerId',
|
||||
_clsiServerId,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [codeCheckFailed] = useDetachStateWatcher(
|
||||
'codeCheckFailed',
|
||||
_codeCheckFailed,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [compiling] = useDetachStateWatcher(
|
||||
'compiling',
|
||||
_compiling,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [draft] = useDetachStateWatcher('draft', _draft, 'detacher', 'detached')
|
||||
const [error] = useDetachStateWatcher('error', _error, 'detacher', 'detached')
|
||||
const [fileList] = useDetachStateWatcher(
|
||||
'fileList',
|
||||
_fileList,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [hasChanges] = useDetachStateWatcher(
|
||||
'hasChanges',
|
||||
_hasChanges,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [highlights] = useDetachStateWatcher(
|
||||
'highlights',
|
||||
_highlights,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [logEntries] = useDetachStateWatcher(
|
||||
'logEntries',
|
||||
_logEntries,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [logEntryAnnotations] = useDetachStateWatcher(
|
||||
'logEntryAnnotations',
|
||||
_logEntryAnnotations,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [pdfDownloadUrl] = useDetachStateWatcher(
|
||||
'pdfDownloadUrl',
|
||||
_pdfDownloadUrl,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [pdfUrl] = useDetachStateWatcher(
|
||||
'pdfUrl',
|
||||
_pdfUrl,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [pdfViewer] = useDetachStateWatcher(
|
||||
'pdfViewer',
|
||||
_pdfViewer,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [position] = useDetachStateWatcher(
|
||||
'position',
|
||||
_position,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [rawLog] = useDetachStateWatcher(
|
||||
'rawLog',
|
||||
_rawLog,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [showLogs] = useDetachStateWatcher(
|
||||
'showLogs',
|
||||
_showLogs,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [stopOnValidationError] = useDetachStateWatcher(
|
||||
'stopOnValidationError',
|
||||
_stopOnValidationError,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [uncompiled] = useDetachStateWatcher(
|
||||
'uncompiled',
|
||||
_uncompiled,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [validationIssues] = useDetachStateWatcher(
|
||||
'validationIssues',
|
||||
_validationIssues,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
|
||||
const setAutoCompile = useDetachAction(
|
||||
'setAutoCompile',
|
||||
_setAutoCompile,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const setDraft = useDetachAction(
|
||||
'setDraft',
|
||||
_setDraft,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const setError = useDetachAction(
|
||||
'setError',
|
||||
_setError,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const setPosition = useDetachAction(
|
||||
'setPosition',
|
||||
_setPosition,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const firstRenderDone = useDetachAction(
|
||||
'firstRenderDone',
|
||||
_firstRenderDone,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const setHasLintingError = useDetachAction(
|
||||
'setHasLintingError',
|
||||
_setHasLintingError,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const setHighlights = useDetachAction(
|
||||
'setHighlights',
|
||||
_setHighlights,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const setShowLogs = useDetachAction(
|
||||
'setShowLogs',
|
||||
_setShowLogs,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const toggleLogs = useDetachAction(
|
||||
'toggleLogs',
|
||||
_toggleLogs,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const setStopOnValidationError = useDetachAction(
|
||||
'setStopOnValidationError',
|
||||
_setStopOnValidationError,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const cleanupCompileResult = useDetachAction(
|
||||
'cleanupCompileResult',
|
||||
_cleanupCompileResult,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const recompileFromScratch = useDetachAction(
|
||||
'recompileFromScratch',
|
||||
_recompileFromScratch,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const setCompiling = useDetachAction(
|
||||
'setCompiling',
|
||||
_setCompiling,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const startCompile = useDetachAction(
|
||||
'startCompile',
|
||||
_startCompile,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const stopCompile = useDetachAction(
|
||||
'stopCompile',
|
||||
_stopCompile,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const setChangedAt = useDetachAction(
|
||||
'setChangedAt',
|
||||
_setChangedAt,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
const clearCache = useDetachAction(
|
||||
'clearCache',
|
||||
_clearCache,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
autoCompile,
|
||||
clearCache,
|
||||
clearingCache,
|
||||
clsiServerId,
|
||||
codeCheckFailed,
|
||||
compiling,
|
||||
draft,
|
||||
error,
|
||||
fileList,
|
||||
hasChanges,
|
||||
highlights,
|
||||
logEntryAnnotations,
|
||||
logEntries,
|
||||
pdfDownloadUrl,
|
||||
pdfUrl,
|
||||
pdfViewer,
|
||||
position,
|
||||
rawLog,
|
||||
recompileFromScratch,
|
||||
setAutoCompile,
|
||||
setCompiling,
|
||||
setDraft,
|
||||
setError,
|
||||
setHasLintingError,
|
||||
setHighlights,
|
||||
setPosition,
|
||||
setShowLogs,
|
||||
toggleLogs,
|
||||
setStopOnValidationError,
|
||||
showLogs,
|
||||
startCompile,
|
||||
stopCompile,
|
||||
stopOnValidationError,
|
||||
uncompiled,
|
||||
validationIssues,
|
||||
firstRenderDone,
|
||||
setChangedAt,
|
||||
cleanupCompileResult,
|
||||
}),
|
||||
[
|
||||
autoCompile,
|
||||
clearCache,
|
||||
clearingCache,
|
||||
clsiServerId,
|
||||
codeCheckFailed,
|
||||
compiling,
|
||||
draft,
|
||||
error,
|
||||
fileList,
|
||||
hasChanges,
|
||||
highlights,
|
||||
logEntryAnnotations,
|
||||
logEntries,
|
||||
pdfDownloadUrl,
|
||||
pdfUrl,
|
||||
pdfViewer,
|
||||
position,
|
||||
rawLog,
|
||||
recompileFromScratch,
|
||||
setAutoCompile,
|
||||
setCompiling,
|
||||
setDraft,
|
||||
setError,
|
||||
setHasLintingError,
|
||||
setHighlights,
|
||||
setPosition,
|
||||
setShowLogs,
|
||||
toggleLogs,
|
||||
setStopOnValidationError,
|
||||
showLogs,
|
||||
startCompile,
|
||||
stopCompile,
|
||||
stopOnValidationError,
|
||||
uncompiled,
|
||||
validationIssues,
|
||||
firstRenderDone,
|
||||
setChangedAt,
|
||||
cleanupCompileResult,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<DetachCompileContext.Provider value={value}>
|
||||
{children}
|
||||
</DetachCompileContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
DetachCompileProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useDetachCompileContext(propTypes) {
|
||||
const data = useContext(DetachCompileContext)
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
data,
|
||||
'data',
|
||||
'DetachCompileContext.Provider'
|
||||
)
|
||||
return data
|
||||
}
|
|
@ -29,6 +29,7 @@ const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
|||
const SYSEND_CHANNEL = `detach-${getMeta('ol-project_id')}`
|
||||
|
||||
export function DetachProvider({ children }) {
|
||||
const [lastDetachedConnectedAt, setLastDetachedConnectedAt] = useState()
|
||||
const [role, setRole] = useState(() => getMeta('ol-detachRole') || null)
|
||||
const {
|
||||
addHandler: addEventHandler,
|
||||
|
@ -94,15 +95,33 @@ export function DetachProvider({ children }) {
|
|||
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
||||
}, [broadcastEvent])
|
||||
|
||||
useEffect(() => {
|
||||
const updateLastDetachedConnectedAt = message => {
|
||||
if (message.role === 'detached' && message.event === 'connected') {
|
||||
setLastDetachedConnectedAt(new Date())
|
||||
}
|
||||
}
|
||||
addEventHandler(updateLastDetachedConnectedAt)
|
||||
return () => deleteEventHandler(updateLastDetachedConnectedAt)
|
||||
}, [addEventHandler, deleteEventHandler])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
role,
|
||||
setRole,
|
||||
broadcastEvent,
|
||||
lastDetachedConnectedAt,
|
||||
addEventHandler,
|
||||
deleteEventHandler,
|
||||
}),
|
||||
[role, setRole, broadcastEvent, addEventHandler, deleteEventHandler]
|
||||
[
|
||||
role,
|
||||
setRole,
|
||||
broadcastEvent,
|
||||
lastDetachedConnectedAt,
|
||||
addEventHandler,
|
||||
deleteEventHandler,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -155,7 +155,7 @@ export function EditorProvider({ children, settings }) {
|
|||
|
||||
EditorProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
settings: PropTypes.any.isRequired,
|
||||
settings: PropTypes.object,
|
||||
}
|
||||
|
||||
export function useEditorContext(propTypes) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { getMockIde } from './mock/mock-ide'
|
||||
|
||||
const IdeContext = createContext()
|
||||
|
||||
|
@ -20,11 +21,13 @@ export function useIdeContext() {
|
|||
}
|
||||
|
||||
export function IdeProvider({ ide, children }) {
|
||||
return <IdeContext.Provider value={ide}>{children}</IdeContext.Provider>
|
||||
const [value] = useState(() => ide || getMockIde())
|
||||
|
||||
return <IdeContext.Provider value={value}>{children}</IdeContext.Provider>
|
||||
}
|
||||
IdeProvider.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
ide: PropTypes.shape({
|
||||
$scope: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -108,11 +108,13 @@ export function LayoutProvider({ children }) {
|
|||
isLinking: detachIsLinking,
|
||||
isLinked: detachIsLinked,
|
||||
role: detachRole,
|
||||
isRedundant: detachIsRedundant,
|
||||
} = useDetachLayout()
|
||||
|
||||
useEffect(() => {
|
||||
if (debugPdfDetach) {
|
||||
console.log('Layout Effect', {
|
||||
detachIsRedundant,
|
||||
detachRole,
|
||||
detachIsLinking,
|
||||
detachIsLinked,
|
||||
|
@ -121,12 +123,23 @@ export function LayoutProvider({ children }) {
|
|||
|
||||
if (detachRole !== 'detacher') return // not in a PDF detacher layout
|
||||
|
||||
if (detachIsRedundant) {
|
||||
changeLayout('sideBySide')
|
||||
return
|
||||
}
|
||||
|
||||
if (detachIsLinking || detachIsLinked) {
|
||||
// the tab is linked to a detached tab (or about to be linked); show
|
||||
// editor only
|
||||
changeLayout('flat', 'editor')
|
||||
}
|
||||
}, [detachRole, detachIsLinking, detachIsLinked, changeLayout])
|
||||
}, [
|
||||
detachIsRedundant,
|
||||
detachRole,
|
||||
detachIsLinking,
|
||||
detachIsLinked,
|
||||
changeLayout,
|
||||
])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
|
|
@ -13,7 +13,11 @@ import useScopeValueSetterOnly from '../hooks/use-scope-value-setter-only'
|
|||
import usePersistedState from '../hooks/use-persisted-state'
|
||||
import useAbortController from '../hooks/use-abort-controller'
|
||||
import DocumentCompiler from '../../features/pdf-preview/util/compiler'
|
||||
import { send, sendMBSampled } from '../../infrastructure/event-tracking'
|
||||
import {
|
||||
send,
|
||||
sendMBOnce,
|
||||
sendMBSampled,
|
||||
} from '../../infrastructure/event-tracking'
|
||||
import {
|
||||
buildLogEntryAnnotations,
|
||||
handleLogFiles,
|
||||
|
@ -24,9 +28,9 @@ import { useProjectContext } from './project-context'
|
|||
import { useEditorContext } from './editor-context'
|
||||
import { buildFileList } from '../../features/pdf-preview/util/file-list'
|
||||
|
||||
export const CompileContext = createContext()
|
||||
export const LocalCompileContext = createContext()
|
||||
|
||||
CompileContext.Provider.propTypes = {
|
||||
export const CompileContextPropTypes = {
|
||||
value: PropTypes.shape({
|
||||
autoCompile: PropTypes.bool.isRequired,
|
||||
clearingCache: PropTypes.bool.isRequired,
|
||||
|
@ -52,6 +56,7 @@ CompileContext.Provider.propTypes = {
|
|||
setHighlights: PropTypes.func.isRequired,
|
||||
setPosition: PropTypes.func.isRequired,
|
||||
setShowLogs: PropTypes.func.isRequired,
|
||||
toggleLogs: PropTypes.func.isRequired,
|
||||
setStopOnValidationError: PropTypes.func.isRequired,
|
||||
showLogs: PropTypes.bool.isRequired,
|
||||
stopOnValidationError: PropTypes.bool.isRequired,
|
||||
|
@ -62,7 +67,9 @@ CompileContext.Provider.propTypes = {
|
|||
}),
|
||||
}
|
||||
|
||||
export function CompileProvider({ children }) {
|
||||
LocalCompileContext.Provider.propTypes = CompileContextPropTypes
|
||||
|
||||
export function LocalCompileProvider({ children }) {
|
||||
const ide = useIdeContext()
|
||||
|
||||
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
|
||||
|
@ -111,6 +118,15 @@ export function CompileProvider({ children }) {
|
|||
// whether the logs should be visible
|
||||
const [showLogs, setShowLogs] = useState(false)
|
||||
|
||||
const toggleLogs = useCallback(() => {
|
||||
setShowLogs(prev => {
|
||||
if (!prev) {
|
||||
sendMBOnce('ide-open-logs-once')
|
||||
}
|
||||
return !prev
|
||||
})
|
||||
}, [setShowLogs])
|
||||
|
||||
// an error that occurred
|
||||
const [error, setError] = useState()
|
||||
|
||||
|
@ -445,6 +461,7 @@ export function CompileProvider({ children }) {
|
|||
setHighlights,
|
||||
setPosition,
|
||||
setShowLogs,
|
||||
toggleLogs,
|
||||
setStopOnValidationError,
|
||||
showLogs,
|
||||
startCompile,
|
||||
|
@ -492,20 +509,29 @@ export function CompileProvider({ children }) {
|
|||
firstRenderDone,
|
||||
setChangedAt,
|
||||
cleanupCompileResult,
|
||||
setShowLogs,
|
||||
toggleLogs,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<CompileContext.Provider value={value}>{children}</CompileContext.Provider>
|
||||
<LocalCompileContext.Provider value={value}>
|
||||
{children}
|
||||
</LocalCompileContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
CompileProvider.propTypes = {
|
||||
LocalCompileProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useCompileContext(propTypes) {
|
||||
const data = useContext(CompileContext)
|
||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'CompileContext.Provider')
|
||||
export function useLocalCompileContext(propTypes) {
|
||||
const data = useContext(LocalCompileContext)
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
data,
|
||||
'data',
|
||||
'LocalCompileContext.Provider'
|
||||
)
|
||||
return data
|
||||
}
|
65
services/web/frontend/js/shared/context/mock/mock-ide.js
Normal file
65
services/web/frontend/js/shared/context/mock/mock-ide.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import getMeta from '../../../utils/meta'
|
||||
|
||||
// When rendered without Angular, ide isn't defined. In that case we use
|
||||
// a mock object that only has the required properties to pass proptypes
|
||||
// checks and the values needed for the app. In the longer term, the mock
|
||||
// object will replace ide completely.
|
||||
export const getMockIde = () => {
|
||||
return {
|
||||
_id: getMeta('ol-project_id'),
|
||||
$scope: {
|
||||
$on: () => {},
|
||||
$watch: () => {},
|
||||
$applyAsync: () => {},
|
||||
user: {},
|
||||
project: {
|
||||
_id: getMeta('ol-project_id'),
|
||||
name: getMeta('ol-projectName'),
|
||||
rootDocId: '',
|
||||
members: [],
|
||||
invites: [],
|
||||
features: {
|
||||
collaborators: 0,
|
||||
compileGroup: 'standard',
|
||||
trackChangesVisible: false,
|
||||
references: false,
|
||||
mendeley: false,
|
||||
zotero: false,
|
||||
},
|
||||
publicAccessLevel: '',
|
||||
tokens: {
|
||||
readOnly: '',
|
||||
readAndWrite: '',
|
||||
},
|
||||
owner: {
|
||||
_id: '',
|
||||
email: '',
|
||||
},
|
||||
},
|
||||
state: { loading: false },
|
||||
permissionsLevel: 'readOnly',
|
||||
editor: {
|
||||
sharejs_doc: null,
|
||||
showSymbolPalette: false,
|
||||
toggleSymbolPalette: () => {},
|
||||
},
|
||||
ui: {
|
||||
view: 'pdf',
|
||||
chatOpen: false,
|
||||
reviewPanelOpen: false,
|
||||
leftMenuShown: false,
|
||||
pdfLayout: 'flat',
|
||||
},
|
||||
pdf: {
|
||||
uncompiled: true,
|
||||
logEntryAnnotations: {},
|
||||
},
|
||||
settings: { syntaxValidation: false, pdfViewer: 'pdfjs' },
|
||||
hasLintingError: false,
|
||||
},
|
||||
editorManager: {
|
||||
openDoc: () => {},
|
||||
getCurrentDocId: () => {},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -4,7 +4,8 @@ import createSharedContext from 'react2angular-shared-context'
|
|||
import { UserProvider } from './user-context'
|
||||
import { IdeProvider } from './ide-context'
|
||||
import { EditorProvider } from './editor-context'
|
||||
import { CompileProvider } from './compile-context'
|
||||
import { LocalCompileProvider } from './local-compile-context'
|
||||
import { DetachCompileProvider } from './detach-compile-context'
|
||||
import { LayoutProvider } from './layout-context'
|
||||
import { DetachProvider } from './detach-context'
|
||||
import { ChatProvider } from '../../features/chat/context/chat-context'
|
||||
|
@ -22,9 +23,11 @@ export function ContextRoot({ children, ide, settings }) {
|
|||
<EditorProvider settings={settings}>
|
||||
<DetachProvider>
|
||||
<LayoutProvider>
|
||||
<CompileProvider>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</CompileProvider>
|
||||
<LocalCompileProvider>
|
||||
<DetachCompileProvider>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</DetachCompileProvider>
|
||||
</LocalCompileProvider>
|
||||
</LayoutProvider>
|
||||
</DetachProvider>
|
||||
</EditorProvider>
|
||||
|
@ -38,8 +41,8 @@ export function ContextRoot({ children, ide, settings }) {
|
|||
|
||||
ContextRoot.propTypes = {
|
||||
children: PropTypes.any,
|
||||
ide: PropTypes.any.isRequired,
|
||||
settings: PropTypes.any.isRequired,
|
||||
ide: PropTypes.object,
|
||||
settings: PropTypes.object,
|
||||
}
|
||||
|
||||
export const rootContext = createSharedContext(ContextRoot)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import getMeta from '../../utils/meta'
|
||||
|
||||
export const UserContext = createContext()
|
||||
|
||||
|
@ -18,7 +18,7 @@ UserContext.Provider.propTypes = {
|
|||
}
|
||||
|
||||
export function UserProvider({ children }) {
|
||||
const [user] = useScopeValue('user', true)
|
||||
const user = getMeta('ol-user')
|
||||
|
||||
return <UserContext.Provider value={user}>{children}</UserContext.Provider>
|
||||
}
|
||||
|
|
|
@ -1,33 +1,21 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
|
||||
export default function useCallbackHandlers() {
|
||||
const [handlers, setHandlers] = useState(new Set())
|
||||
const handlersRef = useRef(new Set())
|
||||
|
||||
const addHandler = useCallback(
|
||||
handler => {
|
||||
setHandlers(prev => new Set(prev.add(handler)))
|
||||
},
|
||||
[setHandlers]
|
||||
)
|
||||
const addHandler = useCallback(handler => {
|
||||
handlersRef.current.add(handler)
|
||||
}, [])
|
||||
|
||||
const deleteHandler = useCallback(
|
||||
handler => {
|
||||
setHandlers(prev => {
|
||||
prev.delete(handler)
|
||||
return new Set(prev)
|
||||
})
|
||||
},
|
||||
[setHandlers]
|
||||
)
|
||||
const deleteHandler = useCallback(handler => {
|
||||
handlersRef.current.delete(handler)
|
||||
}, [])
|
||||
|
||||
const callHandlers = useCallback(
|
||||
(...args) => {
|
||||
for (const handler of handlers) {
|
||||
handler(...args)
|
||||
}
|
||||
},
|
||||
[handlers]
|
||||
)
|
||||
const callHandlers = useCallback((...args) => {
|
||||
for (const handler of handlersRef.current) {
|
||||
handler(...args)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { addHandler, deleteHandler, callHandlers }
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ export default function useDetachLayout() {
|
|||
// isLinked: when the tab is linked to another tab (of different role)
|
||||
const [isLinked, setIsLinked] = useState(false)
|
||||
|
||||
// isRedundant: when a second detacher tab is opened, the first becomes
|
||||
// redundant
|
||||
const [isRedundant, setIsRedundant] = useState(false)
|
||||
|
||||
const uiTimeoutRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -76,12 +80,24 @@ export default function useDetachLayout() {
|
|||
}, [setRole, setIsLinked, broadcastEvent])
|
||||
|
||||
const detach = useCallback(() => {
|
||||
setIsRedundant(false)
|
||||
setRole('detacher')
|
||||
setIsLinking(true)
|
||||
|
||||
window.open(buildUrlWithDetachRole('detached').toString(), '_blank')
|
||||
}, [setRole, setIsLinking])
|
||||
|
||||
const handleEventForDetacherFromDetacher = useCallback(() => {
|
||||
if (debugPdfDetach) {
|
||||
console.log(
|
||||
'Duplicate detacher detected, turning into a regular editor again'
|
||||
)
|
||||
}
|
||||
setIsRedundant(true)
|
||||
setIsLinked(false)
|
||||
setRole(null)
|
||||
}, [setRole, setIsLinked])
|
||||
|
||||
const handleEventForDetacherFromDetached = useCallback(
|
||||
message => {
|
||||
switch (message.event) {
|
||||
|
@ -122,7 +138,7 @@ export default function useDetachLayout() {
|
|||
[setIsLinked, broadcastEvent]
|
||||
)
|
||||
|
||||
const handleEventFromSelf = useCallback(
|
||||
const handleEventForDetachedFromDetached = useCallback(
|
||||
message => {
|
||||
switch (message.event) {
|
||||
case 'closed':
|
||||
|
@ -137,7 +153,7 @@ export default function useDetachLayout() {
|
|||
message => {
|
||||
if (role === 'detacher') {
|
||||
if (message.role === 'detacher') {
|
||||
handleEventFromSelf(message)
|
||||
handleEventForDetacherFromDetacher(message)
|
||||
} else if (message.role === 'detached') {
|
||||
handleEventForDetacherFromDetached(message)
|
||||
}
|
||||
|
@ -145,15 +161,16 @@ export default function useDetachLayout() {
|
|||
if (message.role === 'detacher') {
|
||||
handleEventForDetachedFromDetacher(message)
|
||||
} else if (message.role === 'detached') {
|
||||
handleEventFromSelf(message)
|
||||
handleEventForDetachedFromDetached(message)
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
role,
|
||||
handleEventForDetacherFromDetacher,
|
||||
handleEventForDetacherFromDetached,
|
||||
handleEventForDetachedFromDetacher,
|
||||
handleEventFromSelf,
|
||||
handleEventForDetachedFromDetached,
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -168,5 +185,6 @@ export default function useDetachLayout() {
|
|||
isLinked,
|
||||
isLinking,
|
||||
role,
|
||||
isRedundant,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { useEffect } from 'react'
|
||||
import useDetachState from './use-detach-state'
|
||||
|
||||
export default function useDetachStateWatcher(
|
||||
key,
|
||||
stateValue,
|
||||
senderRole,
|
||||
targetRole
|
||||
) {
|
||||
const [value, setValue] = useDetachState(
|
||||
key,
|
||||
stateValue,
|
||||
senderRole,
|
||||
targetRole
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(stateValue)
|
||||
}, [setValue, stateValue])
|
||||
|
||||
return [value, setValue]
|
||||
}
|
|
@ -12,16 +12,30 @@ export default function useDetachState(
|
|||
) {
|
||||
const [value, setValue] = useState(defaultValue)
|
||||
|
||||
const { role, broadcastEvent, addEventHandler, deleteEventHandler } =
|
||||
useDetachContext()
|
||||
const {
|
||||
role,
|
||||
broadcastEvent,
|
||||
lastDetachedConnectedAt,
|
||||
addEventHandler,
|
||||
deleteEventHandler,
|
||||
} = useDetachContext()
|
||||
|
||||
const eventName = `state-${key}`
|
||||
|
||||
// lastDetachedConnectedAt is added as a dependency in order to re-broadcast
|
||||
// all states when a new detached tab connects
|
||||
useEffect(() => {
|
||||
if (role === senderRole) {
|
||||
broadcastEvent(eventName, { value })
|
||||
}
|
||||
}, [role, senderRole, eventName, value, broadcastEvent])
|
||||
}, [
|
||||
role,
|
||||
senderRole,
|
||||
eventName,
|
||||
value,
|
||||
broadcastEvent,
|
||||
lastDetachedConnectedAt,
|
||||
])
|
||||
|
||||
const handleStateEvent = useCallback(
|
||||
message => {
|
||||
|
|
|
@ -37,7 +37,7 @@ function usePersistedState(key, defaultValue, listen = false) {
|
|||
if (event.key === key) {
|
||||
// note: this value is read via getItem rather than from event.newValue
|
||||
// because getItem handles deserializing the JSON that's stored in localStorage.
|
||||
setValue(localStorage.getItem(key))
|
||||
setValue(localStorage.getItem(key) ?? defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ function usePersistedState(key, defaultValue, listen = false) {
|
|||
window.removeEventListener('storage', listener)
|
||||
}
|
||||
}
|
||||
}, [key, listen])
|
||||
}, [key, listen, defaultValue])
|
||||
|
||||
return [value, updateFunction]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { buildFileList } from '../js/features/pdf-preview/util/file-list'
|
|||
import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
|
||||
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 { useDetachCompileContext as useCompileContext } from '../js/shared/context/detach-compile-context'
|
||||
import {
|
||||
dispatchDocChanged,
|
||||
mockBuildFile,
|
||||
|
|
|
@ -1580,6 +1580,7 @@
|
|||
"project_layout_sharing_submission": "Project Layout, Sharing, and Submission",
|
||||
"pdf_in_separate_tab": "PDF in separate tab",
|
||||
"tab_no_longer_connected": "This tab is no longer connected with the editor",
|
||||
"tab_connecting": "Connecting with the editor",
|
||||
"redirect_to_editor": "Redirect to editor",
|
||||
"layout_processing": "Layout processing",
|
||||
"show_in_code": "Show in code",
|
||||
|
|
|
@ -15,24 +15,27 @@ import { stubMathJax, tearDownMathJaxStubs } from './stubs'
|
|||
import sinon from 'sinon'
|
||||
|
||||
describe('<ChatPane />', function () {
|
||||
const user = {
|
||||
id: 'fake_user',
|
||||
first_name: 'fake_user_first_name',
|
||||
email: 'fake@example.com',
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
this.clock = sinon.useFakeTimers({
|
||||
toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'],
|
||||
})
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', user)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.clock.runAll()
|
||||
this.clock.restore()
|
||||
fetchMock.reset()
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
const user = {
|
||||
id: 'fake_user',
|
||||
first_name: 'fake_user_first_name',
|
||||
email: 'fake@example.com',
|
||||
}
|
||||
|
||||
const testMessages = [
|
||||
{
|
||||
id: 'msg_1',
|
||||
|
|
|
@ -25,10 +25,15 @@ describe('ChatContext', function () {
|
|||
cleanUpContext()
|
||||
|
||||
stubMathJax()
|
||||
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', user)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
tearDownMathJaxStubs()
|
||||
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
describe('socket connection', function () {
|
||||
|
@ -42,7 +47,7 @@ describe('ChatContext', function () {
|
|||
|
||||
it('subscribes when mounted', function () {
|
||||
const socket = new EventEmitter()
|
||||
renderChatContextHook({ user, socket })
|
||||
renderChatContextHook({ socket })
|
||||
|
||||
// Assert that there is 1 listener
|
||||
expect(socket.rawListeners('new-chat-message').length).to.equal(1)
|
||||
|
@ -50,7 +55,7 @@ describe('ChatContext', function () {
|
|||
|
||||
it('unsubscribes when unmounted', function () {
|
||||
const socket = new EventEmitter()
|
||||
const { unmount } = renderChatContextHook({ user, socket })
|
||||
const { unmount } = renderChatContextHook({ socket })
|
||||
|
||||
unmount()
|
||||
|
||||
|
@ -62,7 +67,6 @@ describe('ChatContext', function () {
|
|||
// Mock socket: we only need to emit events, not mock actual connections
|
||||
const socket = new EventEmitter()
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||
user,
|
||||
socket,
|
||||
})
|
||||
|
||||
|
@ -93,7 +97,6 @@ describe('ChatContext', function () {
|
|||
it("doesn't add received messages from the current user if a message was just sent", async function () {
|
||||
const socket = new EventEmitter()
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||
user,
|
||||
socket,
|
||||
})
|
||||
|
||||
|
@ -123,7 +126,6 @@ describe('ChatContext', function () {
|
|||
it('adds the new message from the current user if another message was received after sending', async function () {
|
||||
const socket = new EventEmitter()
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||
user,
|
||||
socket,
|
||||
})
|
||||
|
||||
|
@ -187,7 +189,7 @@ describe('ChatContext', function () {
|
|||
})
|
||||
|
||||
it('adds messages to the list', async function () {
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.loadInitialMessages()
|
||||
await waitForNextUpdate()
|
||||
|
@ -196,7 +198,7 @@ describe('ChatContext', function () {
|
|||
})
|
||||
|
||||
it("won't load messages a second time", async function () {
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.loadInitialMessages()
|
||||
await waitForNextUpdate()
|
||||
|
@ -211,7 +213,7 @@ describe('ChatContext', function () {
|
|||
it('provides an error on failure', async function () {
|
||||
fetchMock.reset()
|
||||
fetchMock.get('express:/project/:projectId/messages', 500)
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.loadInitialMessages()
|
||||
await waitForNextUpdate()
|
||||
|
@ -233,7 +235,7 @@ describe('ChatContext', function () {
|
|||
},
|
||||
])
|
||||
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.loadMoreMessages()
|
||||
await waitForNextUpdate()
|
||||
|
@ -267,7 +269,7 @@ describe('ChatContext', function () {
|
|||
{ overwriteRoutes: false }
|
||||
)
|
||||
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.loadMoreMessages()
|
||||
await waitForNextUpdate()
|
||||
|
@ -297,7 +299,7 @@ describe('ChatContext', function () {
|
|||
createMessages(49, user)
|
||||
)
|
||||
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.loadMoreMessages()
|
||||
await waitForNextUpdate()
|
||||
|
@ -322,7 +324,6 @@ describe('ChatContext', function () {
|
|||
|
||||
const socket = new EventEmitter()
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||
user,
|
||||
socket,
|
||||
})
|
||||
|
||||
|
@ -367,7 +368,7 @@ describe('ChatContext', function () {
|
|||
it('provides an error on failures', async function () {
|
||||
fetchMock.reset()
|
||||
fetchMock.get('express:/project/:projectId/messages', 500)
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.loadMoreMessages()
|
||||
await waitForNextUpdate()
|
||||
|
@ -387,7 +388,7 @@ describe('ChatContext', function () {
|
|||
})
|
||||
|
||||
it('optimistically adds the message to the list', function () {
|
||||
const { result } = renderChatContextHook({ user })
|
||||
const { result } = renderChatContextHook({})
|
||||
|
||||
result.current.sendMessage('sent message')
|
||||
|
||||
|
@ -397,7 +398,7 @@ describe('ChatContext', function () {
|
|||
})
|
||||
|
||||
it('POSTs the message to the backend', function () {
|
||||
const { result } = renderChatContextHook({ user })
|
||||
const { result } = renderChatContextHook({})
|
||||
|
||||
result.current.sendMessage('sent message')
|
||||
|
||||
|
@ -409,7 +410,7 @@ describe('ChatContext', function () {
|
|||
})
|
||||
|
||||
it("doesn't send if the content is empty", function () {
|
||||
const { result } = renderChatContextHook({ user })
|
||||
const { result } = renderChatContextHook({})
|
||||
|
||||
result.current.sendMessage('')
|
||||
|
||||
|
@ -426,7 +427,7 @@ describe('ChatContext', function () {
|
|||
fetchMock
|
||||
.get('express:/project/:projectId/messages', [])
|
||||
.postOnce('express:/project/:projectId/messages', 500)
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
||||
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||
|
||||
result.current.sendMessage('sent message')
|
||||
await waitForNextUpdate()
|
||||
|
@ -444,7 +445,7 @@ describe('ChatContext', function () {
|
|||
|
||||
it('increments unreadMessageCount when a new message is received', function () {
|
||||
const socket = new EventEmitter()
|
||||
const { result } = renderChatContextHook({ user, socket })
|
||||
const { result } = renderChatContextHook({ socket })
|
||||
|
||||
// Receive a new message from the socket
|
||||
socket.emit('new-chat-message', {
|
||||
|
@ -459,7 +460,7 @@ describe('ChatContext', function () {
|
|||
|
||||
it('resets unreadMessageCount when markMessagesAsRead is called', function () {
|
||||
const socket = new EventEmitter()
|
||||
const { result } = renderChatContextHook({ user, socket })
|
||||
const { result } = renderChatContextHook({ socket })
|
||||
|
||||
// Receive a new message from the socket, incrementing unreadMessageCount
|
||||
// by 1
|
||||
|
|
|
@ -18,7 +18,6 @@ describe('<LayoutDropdownButton />', function () {
|
|||
beforeEach(function () {
|
||||
openStub = sinon.stub(window, 'open')
|
||||
window.metaAttributesCache = new Map()
|
||||
fetchMock.post('express:/project/:projectId/compile/stop', () => 204)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -101,11 +100,6 @@ describe('<LayoutDropdownButton />', function () {
|
|||
screen.getByText('Layout processing')
|
||||
})
|
||||
|
||||
it('should stop compile when detaching', function () {
|
||||
expect(fetchMock.called('express:/project/:projectId/compile/stop')).to.be
|
||||
.true
|
||||
})
|
||||
|
||||
it('should record event', function () {
|
||||
sinon.assert.calledWith(eventTrackingSpy.sendMB, 'project-layout-detach')
|
||||
})
|
||||
|
|
|
@ -15,6 +15,8 @@ describe('<FileTreeRoot/>', function () {
|
|||
|
||||
beforeEach(function () {
|
||||
global.requestAnimationFrame = sinon.stub()
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -24,6 +26,7 @@ describe('<FileTreeRoot/>', function () {
|
|||
onInit.reset()
|
||||
cleanUpContext()
|
||||
global.localStorage.clear()
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
it('renders', function () {
|
||||
|
|
|
@ -12,10 +12,16 @@ describe('FileTree Context Menu Flow', function () {
|
|||
const onSelect = sinon.stub()
|
||||
const onInit = sinon.stub()
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
onSelect.reset()
|
||||
onInit.reset()
|
||||
cleanUpContext()
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
it('opens on contextMenu event', async function () {
|
||||
|
|
|
@ -16,6 +16,8 @@ describe('FileTree Create Folder Flow', function () {
|
|||
|
||||
beforeEach(function () {
|
||||
global.requestAnimationFrame = sinon.stub()
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -24,6 +26,7 @@ describe('FileTree Create Folder Flow', function () {
|
|||
onSelect.reset()
|
||||
onInit.reset()
|
||||
cleanUpContext()
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
it('add to root when no files are selected', async function () {
|
||||
|
|
|
@ -14,11 +14,17 @@ describe('FileTree Delete Entity Flow', function () {
|
|||
const onSelect = sinon.stub()
|
||||
const onInit = sinon.stub()
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.restore()
|
||||
onSelect.reset()
|
||||
onInit.reset()
|
||||
cleanUpContext()
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
describe('single entity', function () {
|
||||
|
|
|
@ -16,6 +16,8 @@ describe('FileTree Rename Entity Flow', function () {
|
|||
|
||||
beforeEach(function () {
|
||||
global.requestAnimationFrame = sinon.stub()
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -24,6 +26,7 @@ describe('FileTree Rename Entity Flow', function () {
|
|||
onSelect.reset()
|
||||
onInit.reset()
|
||||
cleanUpContext()
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import DetachCompileButton from '../../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
|
||||
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||
import { screen, fireEvent } from '@testing-library/react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import sysendTestHelper from '../../../helpers/sysend'
|
||||
import { expect } from 'chai'
|
||||
|
||||
|
@ -48,23 +48,4 @@ describe('<DetachCompileButton/>', function () {
|
|||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
||||
it('send compile clicks via detached action', async function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
renderWithEditorContext(<DetachCompileButton />)
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detached',
|
||||
event: 'connected',
|
||||
})
|
||||
|
||||
const compileButton = await screen.getByRole('button', {
|
||||
name: 'Recompile',
|
||||
})
|
||||
fireEvent.click(compileButton)
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detacher',
|
||||
event: 'action-start-compile',
|
||||
data: { args: [] },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,9 +17,7 @@ describe('<PdfLogsEntries/>', function () {
|
|||
message: 'LaTeX Error',
|
||||
content: 'See the LaTeX manual',
|
||||
raw: '',
|
||||
ruleId: 'latex_error',
|
||||
humanReadableHint: '',
|
||||
humanReadableHintComponent: <></>,
|
||||
ruleId: 'hint_misplaced_alignment_tab_character',
|
||||
key: '',
|
||||
},
|
||||
]
|
||||
|
@ -36,6 +34,14 @@ describe('<PdfLogsEntries/>', function () {
|
|||
fileTreeManager.findEntityByPath.resetHistory()
|
||||
})
|
||||
|
||||
it('displays human readable hint', async function () {
|
||||
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
|
||||
fileTreeManager,
|
||||
editorManager,
|
||||
})
|
||||
screen.getByText(/You have placed an alignment tab character/)
|
||||
})
|
||||
|
||||
it('opens doc on click', async function () {
|
||||
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
|
||||
fileTreeManager,
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { expect } from 'chai'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import sysendTestHelper from '../../../helpers/sysend'
|
||||
import PdfPreviewDetachedRoot from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview-detached-root'
|
||||
|
||||
describe('<PdfPreviewDetachedRoot/>', function () {
|
||||
beforeEach(function () {
|
||||
const user = { id: 'user1' }
|
||||
window.user = user
|
||||
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', user)
|
||||
window.metaAttributesCache.set('ol-project_id', 'project1')
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
window.metaAttributesCache.set('ol-projectName', 'Project Name')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
it('syncs compiling state', async function () {
|
||||
render(<PdfPreviewDetachedRoot />)
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'connected',
|
||||
})
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-compiling',
|
||||
data: { value: true },
|
||||
})
|
||||
await screen.findByRole('button', { name: 'Compiling…' })
|
||||
expect(screen.queryByRole('button', { name: 'Recompile' })).to.not.exist
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-compiling',
|
||||
data: { value: false },
|
||||
})
|
||||
await screen.findByRole('button', { name: 'Recompile' })
|
||||
expect(screen.queryByRole('button', { name: 'Compiling…' })).to.not.exist
|
||||
})
|
||||
|
||||
it('sends a clear cache request when the button is pressed', async function () {
|
||||
render(<PdfPreviewDetachedRoot />)
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-showLogs',
|
||||
data: { value: true },
|
||||
})
|
||||
|
||||
const clearCacheButton = await screen.findByRole('button', {
|
||||
name: 'Clear cached files',
|
||||
})
|
||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
||||
|
||||
fireEvent.click(clearCacheButton)
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detached',
|
||||
event: 'action-clearCache',
|
||||
data: {
|
||||
args: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,10 +1,21 @@
|
|||
import sinon from 'sinon'
|
||||
import PdfPreviewHybridToolbar from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
||||
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||
import { screen } from '@testing-library/react'
|
||||
import sysendTestHelper from '../../../helpers/sysend'
|
||||
|
||||
describe('<PdfPreviewHybridToolbar/>', function () {
|
||||
let clock
|
||||
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
sysendTestHelper.resetHistory()
|
||||
clock.runAll()
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('shows normal mode', async function () {
|
||||
|
@ -15,12 +26,49 @@ describe('<PdfPreviewHybridToolbar/>', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('shows orphan mode', async function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
renderWithEditorContext(<PdfPreviewHybridToolbar />)
|
||||
describe('orphan mode', async function () {
|
||||
it('shows connecting message on load', async function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
renderWithEditorContext(<PdfPreviewHybridToolbar />)
|
||||
|
||||
await screen.getByRole('button', {
|
||||
name: 'Redirect to editor',
|
||||
await screen.getByText(/Connecting with the editor/)
|
||||
})
|
||||
|
||||
it('shows compile UI when connected', async function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
renderWithEditorContext(<PdfPreviewHybridToolbar />)
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'connected',
|
||||
})
|
||||
await screen.getByRole('button', {
|
||||
name: 'Recompile',
|
||||
})
|
||||
})
|
||||
|
||||
it('shows connecting message when disconnected', async function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
renderWithEditorContext(<PdfPreviewHybridToolbar />)
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'connected',
|
||||
})
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'closed',
|
||||
})
|
||||
|
||||
await screen.getByText(/Connecting with the editor/)
|
||||
})
|
||||
|
||||
it('shows redirect button after timeout', async function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
renderWithEditorContext(<PdfPreviewHybridToolbar />)
|
||||
clock.tick(6000)
|
||||
|
||||
await screen.getByRole('button', {
|
||||
name: 'Redirect to editor',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -350,7 +350,10 @@ describe('<PdfPreview/>', function () {
|
|||
|
||||
// click the button
|
||||
clearCacheButton.click()
|
||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
||||
await waitFor(() => {
|
||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
||||
})
|
||||
|
@ -382,7 +385,7 @@ describe('<PdfPreview/>', function () {
|
|||
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
||||
|
||||
mockValidPdf()
|
||||
mockClearCache()
|
||||
const finishClearCache = mockDelayed(mockClearCache)
|
||||
|
||||
const recompileFromScratch = screen.getByRole('menuitem', {
|
||||
name: 'Recompile from scratch',
|
||||
|
@ -390,7 +393,11 @@ describe('<PdfPreview/>', function () {
|
|||
})
|
||||
recompileFromScratch.click()
|
||||
|
||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
||||
await waitFor(() => {
|
||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
||||
})
|
||||
|
||||
finishClearCache()
|
||||
|
||||
// wait for compile to finish
|
||||
await screen.findByRole('button', { name: 'Compiling…' })
|
||||
|
|
|
@ -7,7 +7,7 @@ import { fireEvent, screen, waitFor } 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 { useDetachCompileContext as useCompileContext } from '../../../../../frontend/js/shared/context/detach-compile-context'
|
||||
import { useFileTreeData } from '../../../../../frontend/js/shared/context/file-tree-data-context'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
|
@ -122,7 +122,6 @@ const WithSelectedEntities = ({ mockSelectedEntities = [] }) => {
|
|||
|
||||
return null
|
||||
}
|
||||
|
||||
describe('<PdfSynctexControls/>', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
|
@ -185,7 +184,6 @@ describe('<PdfSynctexControls/>', function () {
|
|||
.true
|
||||
})
|
||||
})
|
||||
|
||||
it('disables button when multiple entities are selected', async function () {
|
||||
renderWithEditorContext(
|
||||
<>
|
||||
|
@ -236,9 +234,14 @@ describe('<PdfSynctexControls/>', function () {
|
|||
})
|
||||
|
||||
it('does not have go to PDF location button nor arrow icon', async function () {
|
||||
const { container } = renderWithEditorContext(<PdfSynctexControls />, {
|
||||
scope,
|
||||
})
|
||||
const { container } = renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.queryByRole('button', {
|
||||
|
@ -249,7 +252,50 @@ describe('<PdfSynctexControls/>', function () {
|
|||
expect(container.querySelector('.synctex-control-icon')).to.not.exist
|
||||
})
|
||||
|
||||
it('send go to PDF location action', async function () {
|
||||
it('send set highlights action', async function () {
|
||||
renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
sysendTestHelper.resetHistory()
|
||||
|
||||
const syncToPdfButton = await screen.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
})
|
||||
|
||||
// mock editor cursor position update
|
||||
fireEvent(
|
||||
window,
|
||||
new CustomEvent('cursor:editor:update', {
|
||||
detail: { row: 100, column: 10 },
|
||||
})
|
||||
)
|
||||
|
||||
expect(syncToPdfButton.disabled).to.be.false
|
||||
|
||||
fireEvent.click(syncToPdfButton)
|
||||
|
||||
expect(syncToPdfButton.disabled).to.be.true
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
|
||||
.true
|
||||
})
|
||||
|
||||
// synctex is called locally and the result are broadcast for the detached
|
||||
// tab
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detacher',
|
||||
event: 'action-setHighlights',
|
||||
data: { args: [mockHighlights] },
|
||||
})
|
||||
})
|
||||
|
||||
it('reacts to sync to code action', async function () {
|
||||
renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
|
@ -259,74 +305,23 @@ describe('<PdfSynctexControls/>', function () {
|
|||
{ scope }
|
||||
)
|
||||
|
||||
sysendTestHelper.resetHistory()
|
||||
|
||||
const syncToPdfButton = await screen.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/compile')).to.be
|
||||
.true
|
||||
})
|
||||
|
||||
// mock editor cursor position update
|
||||
fireEvent(
|
||||
window,
|
||||
new CustomEvent('cursor:editor:update', {
|
||||
detail: { row: 100, column: 10 },
|
||||
})
|
||||
)
|
||||
|
||||
fireEvent.click(syncToPdfButton)
|
||||
|
||||
// the button is only disabled when the state is updated via sysend
|
||||
expect(syncToPdfButton.disabled).to.be.false
|
||||
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detacher',
|
||||
event: 'action-go-to-pdf-location',
|
||||
data: { args: ['file=&line=101&column=10'] },
|
||||
})
|
||||
})
|
||||
|
||||
it('update inflight state', async function () {
|
||||
const { container } = renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
sysendTestHelper.resetHistory()
|
||||
|
||||
const syncToPdfButton = await screen.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
})
|
||||
|
||||
// mock editor cursor position update
|
||||
fireEvent(
|
||||
window,
|
||||
new CustomEvent('cursor:editor:update', {
|
||||
detail: { row: 100, column: 10 },
|
||||
})
|
||||
)
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detached',
|
||||
event: 'state-sync-to-pdf-inflight',
|
||||
data: { value: true },
|
||||
event: 'action-sync-to-code',
|
||||
data: {
|
||||
args: [mockPosition],
|
||||
},
|
||||
})
|
||||
expect(syncToPdfButton.disabled).to.be.true
|
||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||
1
|
||||
)
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detached',
|
||||
event: 'state-sync-to-pdf-inflight',
|
||||
data: { value: false },
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
|
||||
.true
|
||||
})
|
||||
expect(syncToPdfButton.disabled).to.be.false
|
||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||
0
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -336,9 +331,13 @@ describe('<PdfSynctexControls/>', function () {
|
|||
})
|
||||
|
||||
it('does not have go to code location button nor arrow icon', async function () {
|
||||
const { container } = renderWithEditorContext(<PdfSynctexControls />, {
|
||||
scope,
|
||||
})
|
||||
const { container } = renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.queryByRole('button', {
|
||||
|
@ -349,102 +348,90 @@ describe('<PdfSynctexControls/>', function () {
|
|||
expect(container.querySelector('.synctex-control-icon')).to.not.exist
|
||||
})
|
||||
|
||||
it('send go to code line action and update inflight state', async function () {
|
||||
it('send go to code line action', async function () {
|
||||
const { container } = renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
sysendTestHelper.resetHistory()
|
||||
|
||||
const syncToCodeButton = await screen.findByRole('button', {
|
||||
name: /Go to PDF location in code/,
|
||||
})
|
||||
expect(syncToCodeButton.disabled).to.be.true
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detached',
|
||||
event: 'state-has-single-selected-doc',
|
||||
data: { value: true },
|
||||
})
|
||||
expect(syncToCodeButton.disabled).to.be.false
|
||||
|
||||
sysendTestHelper.resetHistory()
|
||||
|
||||
fireEvent.click(syncToCodeButton)
|
||||
|
||||
// the button is only disabled when the state is updated via sysend
|
||||
expect(syncToCodeButton.disabled).to.be.false
|
||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||
0
|
||||
)
|
||||
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detached',
|
||||
event: 'action-sync-to-code',
|
||||
data: {
|
||||
args: [mockPosition, 72],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('update inflight state', async function () {
|
||||
const { container } = renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detached',
|
||||
event: 'state-has-single-selected-doc',
|
||||
data: { value: true },
|
||||
})
|
||||
|
||||
const syncToCodeButton = await screen.findByRole('button', {
|
||||
name: /Go to PDF location in code/,
|
||||
})
|
||||
|
||||
expect(syncToCodeButton.disabled).to.be.false
|
||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||
0
|
||||
)
|
||||
|
||||
fireEvent.click(syncToCodeButton)
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-sync-to-code-inflight',
|
||||
data: { value: true },
|
||||
})
|
||||
|
||||
expect(syncToCodeButton.disabled).to.be.true
|
||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||
1
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
|
||||
.true
|
||||
})
|
||||
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detached',
|
||||
event: 'action-go-to-code-line',
|
||||
data: { args: ['main.tex', 100] },
|
||||
})
|
||||
})
|
||||
|
||||
it('sends PDF exists state', async function () {
|
||||
renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
sysendTestHelper.resetHistory()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/compile')).to.be
|
||||
.true
|
||||
})
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detached',
|
||||
event: 'state-pdf-exists',
|
||||
data: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('reacts to go to PDF location action', async function () {
|
||||
renderWithEditorContext(
|
||||
<>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</>,
|
||||
{ scope }
|
||||
)
|
||||
sysendTestHelper.resetHistory()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/compile')).to.be
|
||||
.true
|
||||
})
|
||||
sysendTestHelper.spy.broadcast.resetHistory()
|
||||
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'action-go-to-pdf-location',
|
||||
data: { args: ['file=&line=101&column=10'] },
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
|
||||
.true
|
||||
})
|
||||
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detached',
|
||||
event: 'state-sync-to-pdf-inflight',
|
||||
event: 'state-sync-to-code-inflight',
|
||||
data: { value: false },
|
||||
})
|
||||
|
||||
expect(syncToCodeButton.disabled).to.be.false
|
||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||
0
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -82,8 +82,15 @@ export const mockValidationProblems = validationProblems =>
|
|||
},
|
||||
})
|
||||
|
||||
export const mockClearCache = () =>
|
||||
fetchMock.delete('express:/project/:projectId/output', 204)
|
||||
export const mockClearCache = (delayPromise = Promise.resolve()) =>
|
||||
fetchMock.delete(
|
||||
'express:/project/:projectId/output',
|
||||
delayPromise.then(() => ({
|
||||
body: {
|
||||
status: 204,
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
export const mockValidPdf = () => {
|
||||
nock('https://clsi.test-overleaf.com')
|
||||
|
|
|
@ -83,11 +83,14 @@ describe('<ShareProjectModal/>', function () {
|
|||
|
||||
beforeEach(function () {
|
||||
fetchMock.get('/user/contacts', { contacts })
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-user', { allowedFreeTrial: true })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.restore()
|
||||
cleanUpContext()
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
it('renders the modal', async function () {
|
||||
|
@ -179,7 +182,7 @@ describe('<ShareProjectModal/>', function () {
|
|||
]
|
||||
|
||||
// render as admin: actions should be present
|
||||
const { rerender } = render(
|
||||
render(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
|
@ -197,7 +200,7 @@ describe('<ShareProjectModal/>', function () {
|
|||
await screen.findByRole('button', { name: 'Resend' })
|
||||
|
||||
// render as non-admin (non-owner), link sharing on: actions should be missing and message should be present
|
||||
rerender(
|
||||
render(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
|
@ -222,7 +225,7 @@ describe('<ShareProjectModal/>', function () {
|
|||
expect(screen.queryByRole('button', { name: 'Resend' })).to.be.null
|
||||
|
||||
// render as non-admin (non-owner), link sharing off: actions should be missing and message should be present
|
||||
rerender(
|
||||
render(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
|
@ -619,10 +622,6 @@ describe('<ShareProjectModal/>', function () {
|
|||
)
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
user: {
|
||||
id: '123abd',
|
||||
allowedFreeTrial: true,
|
||||
},
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
|
|
|
@ -10,7 +10,8 @@ import { FileTreeDataProvider } from '../../../frontend/js/shared/context/file-t
|
|||
import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
|
||||
import { DetachProvider } from '../../../frontend/js/shared/context/detach-context'
|
||||
import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
|
||||
import { CompileProvider } from '../../../frontend/js/shared/context/compile-context'
|
||||
import { LocalCompileProvider } from '../../../frontend/js/shared/context/local-compile-context'
|
||||
import { DetachCompileProvider } from '../../../frontend/js/shared/context/detach-compile-context'
|
||||
|
||||
// these constants can be imported in tests instead of
|
||||
// using magic strings
|
||||
|
@ -110,7 +111,9 @@ export function EditorProviders({
|
|||
<EditorProvider settings={{}}>
|
||||
<DetachProvider>
|
||||
<LayoutProvider>
|
||||
<CompileProvider>{children}</CompileProvider>
|
||||
<LocalCompileProvider>
|
||||
<DetachCompileProvider>{children}</DetachCompileProvider>
|
||||
</LocalCompileProvider>
|
||||
</LayoutProvider>
|
||||
</DetachProvider>
|
||||
</EditorProvider>
|
||||
|
|
|
@ -25,6 +25,10 @@ function getLastBroacastMessage() {
|
|||
return getLastDetachCall('broadcast').args[1]
|
||||
}
|
||||
|
||||
function getAllBroacastMessages() {
|
||||
return getDetachCalls('broadcast')
|
||||
}
|
||||
|
||||
// this fakes receiving a message by calling the handler add to `on`. A bit
|
||||
// funky, but works for now
|
||||
function receiveMessage(message) {
|
||||
|
@ -37,5 +41,6 @@ export default {
|
|||
getDetachCalls,
|
||||
getLastDetachCall,
|
||||
getLastBroacastMessage,
|
||||
getAllBroacastMessages,
|
||||
receiveMessage,
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ describe('useDetachLayout', function () {
|
|||
})
|
||||
|
||||
it('detacher role', async function () {
|
||||
sysendTestHelper.spy.broadcast.resetHistory()
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
|
||||
// 1. create hook in detacher mode
|
||||
|
@ -55,6 +56,8 @@ describe('useDetachLayout', function () {
|
|||
expect(result.current.isLinked).to.be.false
|
||||
expect(result.current.isLinking).to.be.false
|
||||
expect(result.current.role).to.equal('detacher')
|
||||
const broadcastMessagesCount =
|
||||
sysendTestHelper.getAllBroacastMessages().length
|
||||
|
||||
// 2. simulate connected detached tab
|
||||
sysendTestHelper.spy.broadcast.resetHistory()
|
||||
|
@ -70,6 +73,12 @@ describe('useDetachLayout', function () {
|
|||
expect(result.current.isLinking).to.be.false
|
||||
expect(result.current.role).to.equal('detacher')
|
||||
|
||||
// check that all message were re-broadcast for the new tab
|
||||
await nextTick() // necessary to ensure all event handler have run
|
||||
const reBroadcastMessagesCount =
|
||||
sysendTestHelper.getAllBroacastMessages().length
|
||||
expect(reBroadcastMessagesCount).to.equal(broadcastMessagesCount)
|
||||
|
||||
// 3. simulate closed detached tab
|
||||
sysendTestHelper.spy.broadcast.resetHistory()
|
||||
sysendTestHelper.receiveMessage({
|
||||
|
@ -90,21 +99,7 @@ describe('useDetachLayout', function () {
|
|||
expect(result.current.isLinking).to.be.false
|
||||
expect(result.current.role).to.equal('detacher')
|
||||
|
||||
// 5. simulate closed detacher tab
|
||||
sysendTestHelper.spy.broadcast.resetHistory()
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'closed',
|
||||
})
|
||||
expect(result.current.isLinked).to.be.true
|
||||
expect(result.current.isLinking).to.be.false
|
||||
expect(result.current.role).to.equal('detacher')
|
||||
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
|
||||
role: 'detacher',
|
||||
event: 'up',
|
||||
})
|
||||
|
||||
// 6. reattach
|
||||
// 5. reattach
|
||||
sysendTestHelper.spy.broadcast.resetHistory()
|
||||
act(() => {
|
||||
result.current.reattach()
|
||||
|
@ -118,6 +113,26 @@ describe('useDetachLayout', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('reset detacher role when other detacher tab connects', function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
|
||||
// 1. create hook in detacher mode
|
||||
const { result } = renderHookWithEditorContext(() => useDetachLayout())
|
||||
expect(result.current.reattach).to.be.a('function')
|
||||
expect(result.current.detach).to.be.a('function')
|
||||
expect(result.current.isLinked).to.be.false
|
||||
expect(result.current.isLinking).to.be.false
|
||||
expect(result.current.role).to.equal('detacher')
|
||||
|
||||
// 2. simulate other detacher tab
|
||||
sysendTestHelper.receiveMessage({
|
||||
role: 'detacher',
|
||||
event: 'up',
|
||||
})
|
||||
expect(result.current.isRedundant).to.be.true
|
||||
expect(result.current.role).to.equal(null)
|
||||
})
|
||||
|
||||
it('detached role', async function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
|
||||
|
@ -185,3 +200,9 @@ describe('useDetachLayout', function () {
|
|||
sinon.assert.called(closeStub)
|
||||
})
|
||||
})
|
||||
|
||||
const nextTick = () => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ const entryPoints = {
|
|||
serviceWorker: './frontend/js/serviceWorker.js',
|
||||
main: './frontend/js/main.js',
|
||||
ide: './frontend/js/ide.js',
|
||||
'ide-detached': './frontend/js/ide-detached.js',
|
||||
marketing: './frontend/js/marketing.js',
|
||||
style: './frontend/stylesheets/style.less',
|
||||
'ieee-style': './frontend/stylesheets/ieee-style.less',
|
||||
|
|
Loading…
Reference in a new issue