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') ||
|
!Features.hasFeature('saas') ||
|
||||||
(user.features && user.features.symbolPalette)
|
(user.features && user.features.symbolPalette)
|
||||||
|
|
||||||
res.render('project/editor', {
|
const template =
|
||||||
|
detachRole === 'detached'
|
||||||
|
? 'project/editor_detached'
|
||||||
|
: 'project/editor'
|
||||||
|
res.render(template, {
|
||||||
title: project.name,
|
title: project.name,
|
||||||
priority_title: true,
|
priority_title: true,
|
||||||
bodyClasses: ['editor'],
|
bodyClasses: ['editor'],
|
||||||
project_id: project._id,
|
project_id: project._id,
|
||||||
|
projectName: project.name,
|
||||||
user: {
|
user: {
|
||||||
id: userId,
|
id: userId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|
|
@ -65,11 +65,7 @@ block content
|
||||||
span.sr-only #{translate("close")}
|
span.sr-only #{translate("close")}
|
||||||
.system-message-content(ng-bind-html="htmlContent")
|
.system-message-content(ng-bind-html="htmlContent")
|
||||||
|
|
||||||
if detachRole === 'detached'
|
include ./editor/main
|
||||||
div.full-size
|
|
||||||
pdf-preview()
|
|
||||||
else
|
|
||||||
include ./editor/main
|
|
||||||
|
|
||||||
script(type="text/ng-template", id="genericMessageModalTemplate")
|
script(type="text/ng-template", id="genericMessageModalTemplate")
|
||||||
.modal-header
|
.modal-header
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
meta(name="ol-useV2History" data-type="boolean" content=useV2History)
|
meta(name="ol-useV2History" data-type="boolean" content=useV2History)
|
||||||
meta(name="ol-project_id" content=project_id)
|
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-userSettings" data-type="json" content=userSettings)
|
||||||
meta(name="ol-user" data-type="json" content=user)
|
meta(name="ol-user" data-type="json" content=user)
|
||||||
meta(name="ol-learnedWords" data-type="json" content=learnedWords)
|
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_project_to_github_explanation": "",
|
||||||
"sync_to_dropbox": "",
|
"sync_to_dropbox": "",
|
||||||
"sync_to_github": "",
|
"sync_to_github": "",
|
||||||
|
"tab_connecting": "",
|
||||||
"tab_no_longer_connected": "",
|
"tab_no_longer_connected": "",
|
||||||
"tags": "",
|
"tags": "",
|
||||||
"template_approved_by_publisher": "",
|
"template_approved_by_publisher": "",
|
||||||
|
|
|
@ -7,7 +7,6 @@ import IconChecked from '../../../shared/components/icon-checked'
|
||||||
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
||||||
import IconEditorOnly from './icon-editor-only'
|
import IconEditorOnly from './icon-editor-only'
|
||||||
import IconPdfOnly from './icon-pdf-only'
|
import IconPdfOnly from './icon-pdf-only'
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
|
||||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||||
|
|
||||||
|
@ -59,13 +58,10 @@ function LayoutDropdownButton() {
|
||||||
pdfLayout,
|
pdfLayout,
|
||||||
} = useLayoutContext(layoutContextPropTypes)
|
} = useLayoutContext(layoutContextPropTypes)
|
||||||
|
|
||||||
const { stopCompile } = useCompileContext()
|
|
||||||
|
|
||||||
const handleDetach = useCallback(() => {
|
const handleDetach = useCallback(() => {
|
||||||
detach()
|
detach()
|
||||||
stopCompile()
|
|
||||||
eventTracking.sendMB('project-layout-detach')
|
eventTracking.sendMB('project-layout-detach')
|
||||||
}, [detach, stopCompile])
|
}, [detach])
|
||||||
|
|
||||||
const handleReattach = useCallback(() => {
|
const handleReattach = useCallback(() => {
|
||||||
if (detachRole !== 'detacher') {
|
if (detachRole !== 'detacher') {
|
||||||
|
|
|
@ -1,26 +1,13 @@
|
||||||
import { memo, useCallback } from 'react'
|
import { memo } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||||
import useDetachAction from '../../../shared/hooks/use-detach-action'
|
|
||||||
import PdfCompileButtonInner from './pdf-compile-button-inner'
|
import PdfCompileButtonInner from './pdf-compile-button-inner'
|
||||||
|
|
||||||
export function DetachCompileButton() {
|
export function DetachCompileButton() {
|
||||||
const { compiling, hasChanges, startCompile } = useCompileContext()
|
const { compiling, hasChanges, startCompile } = useCompileContext()
|
||||||
|
|
||||||
const startOrTriggerCompile = useDetachAction(
|
|
||||||
'start-compile',
|
|
||||||
startCompile,
|
|
||||||
'detacher',
|
|
||||||
'detached'
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleStartCompile = useCallback(
|
|
||||||
() => startOrTriggerCompile(),
|
|
||||||
[startOrTriggerCompile]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames({
|
className={classnames({
|
||||||
|
@ -29,7 +16,7 @@ export function DetachCompileButton() {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<PdfCompileButtonInner
|
<PdfCompileButtonInner
|
||||||
startCompile={handleStartCompile}
|
startCompile={startCompile}
|
||||||
compiling={compiling}
|
compiling={compiling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Icon from '../../../shared/components/icon'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||||
|
|
||||||
function PdfClearCacheButton() {
|
function PdfClearCacheButton() {
|
||||||
const { compiling, clearCache, clearingCache } = useCompileContext()
|
const { compiling, clearCache, clearingCache } = useCompileContext()
|
||||||
|
@ -14,7 +14,7 @@ function PdfClearCacheButton() {
|
||||||
bsSize="small"
|
bsSize="small"
|
||||||
bsStyle="danger"
|
bsStyle="danger"
|
||||||
className="logs-pane-actions-clear-cache"
|
className="logs-pane-actions-clear-cache"
|
||||||
onClick={clearCache}
|
onClick={() => clearCache()}
|
||||||
disabled={clearingCache || compiling}
|
disabled={clearingCache || compiling}
|
||||||
>
|
>
|
||||||
{clearingCache ? <Icon type="refresh" spin /> : <Icon type="trash-o" />}
|
{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 { useTranslation } from 'react-i18next'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import classnames from 'classnames'
|
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'
|
import PdfCompileButtonInner from './pdf-compile-button-inner'
|
||||||
|
|
||||||
function PdfCompileButton() {
|
function PdfCompileButton() {
|
||||||
|
@ -84,7 +84,7 @@ function PdfCompileButton() {
|
||||||
<MenuItem divider />
|
<MenuItem divider />
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onSelect={stopCompile}
|
onSelect={() => stopCompile()}
|
||||||
disabled={!compiling}
|
disabled={!compiling}
|
||||||
aria-disabled={!compiling}
|
aria-disabled={!compiling}
|
||||||
>
|
>
|
||||||
|
@ -92,7 +92,7 @@ function PdfCompileButton() {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onSelect={recompileFromScratch}
|
onSelect={() => recompileFromScratch()}
|
||||||
disabled={compiling}
|
disabled={compiling}
|
||||||
aria-disabled={compiling}
|
aria-disabled={compiling}
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PdfFileList from './pdf-file-list'
|
||||||
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||||
|
|
||||||
function PdfDownloadFilesButton() {
|
function PdfDownloadFilesButton() {
|
||||||
const { compiling, fileList } = useCompileContext()
|
const { compiling, fileList } = useCompileContext()
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import { memo, useCallback } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import { sendMBOnce } from '../../../infrastructure/event-tracking'
|
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import Icon from '../../../shared/components/icon'
|
import Icon from '../../../shared/components/icon'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||||
|
|
||||||
function PdfHybridCodeCheckButton() {
|
function PdfHybridCodeCheckButton() {
|
||||||
const { codeCheckFailed, error, setShowLogs } = useCompileContext()
|
const { codeCheckFailed, error, toggleLogs } = useCompileContext()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setShowLogs(value => {
|
toggleLogs()
|
||||||
if (!value) {
|
}, [toggleLogs])
|
||||||
sendMBOnce('ide-open-logs-once')
|
|
||||||
}
|
|
||||||
|
|
||||||
return !value
|
|
||||||
})
|
|
||||||
}, [setShowLogs])
|
|
||||||
|
|
||||||
if (!codeCheckFailed) {
|
if (!codeCheckFailed) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
|
||||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
import Icon from '../../../shared/components/icon'
|
import Icon from '../../../shared/components/icon'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||||
|
|
||||||
function PdfHybridDownloadButton() {
|
function PdfHybridDownloadButton() {
|
||||||
const { pdfDownloadUrl } = useCompileContext()
|
const { pdfDownloadUrl } = useCompileContext()
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import { memo, useCallback } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button, Label, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
import { Button, Label, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
import { sendMBOnce } from '../../../infrastructure/event-tracking'
|
|
||||||
import Icon from '../../../shared/components/icon'
|
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() {
|
function PdfHybridLogsButton() {
|
||||||
const { error, logEntries, setShowLogs, showLogs } = useCompileContext()
|
const { error, logEntries, toggleLogs, showLogs } = useCompileContext()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setShowLogs(value => {
|
toggleLogs()
|
||||||
if (!value) {
|
}, [toggleLogs])
|
||||||
sendMBOnce('ide-open-logs-once')
|
|
||||||
}
|
|
||||||
|
|
||||||
return !value
|
|
||||||
})
|
|
||||||
}, [setShowLogs])
|
|
||||||
|
|
||||||
const errorCount = Number(logEntries?.errors?.length)
|
const errorCount = Number(logEntries?.errors?.length)
|
||||||
const warningCount = Number(logEntries?.warnings?.length)
|
const warningCount = Number(logEntries?.warnings?.length)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { buildHighlightElement } from '../util/highlights'
|
||||||
import PDFJSWrapper from '../util/pdf-js-wrapper'
|
import PDFJSWrapper from '../util/pdf-js-wrapper'
|
||||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||||
import ErrorBoundaryFallback from './error-boundary-fallback'
|
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'
|
import getMeta from '../../../utils/meta'
|
||||||
|
|
||||||
function PdfJsViewer({ url }) {
|
function PdfJsViewer({ url }) {
|
||||||
|
|
|
@ -3,8 +3,10 @@ import classNames from 'classnames'
|
||||||
import { memo, useCallback } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-header'
|
import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-header'
|
||||||
import PdfLogEntryContent from './pdf-log-entry-content'
|
import PdfLogEntryContent from './pdf-log-entry-content'
|
||||||
|
import HumanReadableLogsHints from '../../../ide/human-readable-logs/HumanReadableLogsHints'
|
||||||
|
|
||||||
function PdfLogEntry({
|
function PdfLogEntry({
|
||||||
|
ruleId,
|
||||||
headerTitle,
|
headerTitle,
|
||||||
headerIcon,
|
headerIcon,
|
||||||
rawContent,
|
rawContent,
|
||||||
|
@ -20,6 +22,12 @@ function PdfLogEntry({
|
||||||
onSourceLocationClick,
|
onSourceLocationClick,
|
||||||
onClose,
|
onClose,
|
||||||
}) {
|
}) {
|
||||||
|
if (ruleId && HumanReadableLogsHints[ruleId]) {
|
||||||
|
const hint = HumanReadableLogsHints[ruleId]
|
||||||
|
formattedContent = hint.formattedContent
|
||||||
|
extraInfoURL = hint.extraInfoURL
|
||||||
|
}
|
||||||
|
|
||||||
const handleLogEntryLinkClick = useCallback(
|
const handleLogEntryLinkClick = useCallback(
|
||||||
event => {
|
event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -56,6 +64,7 @@ function PdfLogEntry({
|
||||||
}
|
}
|
||||||
|
|
||||||
PdfLogEntry.propTypes = {
|
PdfLogEntry.propTypes = {
|
||||||
|
ruleId: PropTypes.string,
|
||||||
sourceLocation: PreviewLogEntryHeader.propTypes.sourceLocation,
|
sourceLocation: PreviewLogEntryHeader.propTypes.sourceLocation,
|
||||||
headerTitle: PropTypes.string,
|
headerTitle: PropTypes.string,
|
||||||
headerIcon: PropTypes.element,
|
headerIcon: PropTypes.element,
|
||||||
|
|
|
@ -48,11 +48,10 @@ function PdfLogsEntries({ entries, hasErrors }) {
|
||||||
{logEntries.map(logEntry => (
|
{logEntries.map(logEntry => (
|
||||||
<PdfLogEntry
|
<PdfLogEntry
|
||||||
key={logEntry.key}
|
key={logEntry.key}
|
||||||
|
ruleId={logEntry.ruleId}
|
||||||
headerTitle={logEntry.message}
|
headerTitle={logEntry.message}
|
||||||
rawContent={logEntry.content}
|
rawContent={logEntry.content}
|
||||||
logType={logEntry.type}
|
logType={logEntry.type}
|
||||||
formattedContent={logEntry.humanReadableHintComponent}
|
|
||||||
extraInfoURL={logEntry.extraInfoURL}
|
|
||||||
level={logEntry.level}
|
level={logEntry.level}
|
||||||
entryAriaLabel={t('log_entry_description', {
|
entryAriaLabel={t('log_entry_description', {
|
||||||
level: logEntry.level,
|
level: logEntry.level,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||||
import ErrorBoundaryFallback from './error-boundary-fallback'
|
import ErrorBoundaryFallback from './error-boundary-fallback'
|
||||||
import PdfCodeCheckFailedNotice from './pdf-code-check-failed-notice'
|
import PdfCodeCheckFailedNotice from './pdf-code-check-failed-notice'
|
||||||
import PdfLogsPaneInfoNotice from './pdf-logs-pane-info-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'
|
import PdfLogEntry from './pdf-log-entry'
|
||||||
|
|
||||||
function PdfLogsViewer() {
|
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 { ButtonToolbar } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
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 PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
|
||||||
import PdfOrphanRefreshButton from './pdf-orphan-refresh-button'
|
import PdfOrphanRefreshButton from './pdf-orphan-refresh-button'
|
||||||
import { DetachedSynctexControl } from './detach-synctex-control'
|
import { DetachedSynctexControl } from './detach-synctex-control'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
|
||||||
|
const ORPHAN_UI_TIMEOUT_MS = 5000
|
||||||
|
|
||||||
function PdfPreviewHybridToolbar() {
|
function PdfPreviewHybridToolbar() {
|
||||||
const { detachRole, detachIsLinked } = useLayoutContext()
|
const { detachRole, detachIsLinked } = useLayoutContext()
|
||||||
|
|
||||||
|
const uiTimeoutRef = useRef()
|
||||||
|
const [orphanPdfTabAfterDelay, setOrphanPdfTabAfterDelay] = useState(false)
|
||||||
|
|
||||||
const orphanPdfTab = !detachIsLinked && detachRole === 'detached'
|
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 (
|
return (
|
||||||
<ButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
|
<ButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
|
||||||
{orphanPdfTab ? (
|
{ToolbarInner}
|
||||||
<PdfPreviewHybridToolbarOrphanInner />
|
|
||||||
) : (
|
|
||||||
<PdfPreviewHybridToolbarInner />
|
|
||||||
)}
|
|
||||||
</ButtonToolbar>
|
</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)
|
export default memo(PdfPreviewHybridToolbar)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PdfLogsViewer from './pdf-logs-viewer'
|
||||||
import PdfViewer from './pdf-viewer'
|
import PdfViewer from './pdf-viewer'
|
||||||
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
||||||
import PdfHybridPreviewToolbar from './pdf-preview-hybrid-toolbar'
|
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() {
|
function PdfPreviewPane() {
|
||||||
const { pdfUrl } = useCompileContext()
|
const { pdfUrl } = useCompileContext()
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import classNames from 'classnames'
|
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 PropTypes from 'prop-types'
|
||||||
import { useIdeContext } from '../../../shared/context/ide-context'
|
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||||
import { useProjectContext } from '../../../shared/context/project-context'
|
import { useProjectContext } from '../../../shared/context/project-context'
|
||||||
import { getJSON } from '../../../infrastructure/fetch-json'
|
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 { useLayoutContext } from '../../../shared/context/layout-context'
|
||||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
|
@ -134,32 +134,19 @@ function PdfSynctexControls() {
|
||||||
|
|
||||||
const { signal } = useAbortController()
|
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(() => {
|
useEffect(() => {
|
||||||
const listener = event => setCursorPosition(event.detail)
|
const listener = event => setCursorPosition(event.detail)
|
||||||
window.addEventListener('cursor:editor:update', listener)
|
window.addEventListener('cursor:editor:update', listener)
|
||||||
return () => window.removeEventListener('cursor:editor:update', listener)
|
return () => window.removeEventListener('cursor:editor:update', listener)
|
||||||
}, [ide])
|
}, [ide])
|
||||||
|
|
||||||
const [syncToPdfInFlight, setSyncToPdfInFlight] = useDetachState(
|
const [syncToPdfInFlight, setSyncToPdfInFlight] = useState(false)
|
||||||
'sync-to-pdf-inflight',
|
const [syncToCodeInFlight, setSyncToCodeInFlight] = useDetachState(
|
||||||
|
'sync-to-code-inflight',
|
||||||
false,
|
false,
|
||||||
'detached',
|
'detacher',
|
||||||
'detacher'
|
'detached'
|
||||||
)
|
)
|
||||||
const [syncToCodeInFlight, setSyncToCodeInFlight] = useState(false)
|
|
||||||
|
|
||||||
const [, setSynctexError] = useScopeValue('sync_tex_error')
|
const [, setSynctexError] = useScopeValue('sync_tex_error')
|
||||||
|
|
||||||
|
@ -179,7 +166,7 @@ function PdfSynctexControls() {
|
||||||
return path
|
return path
|
||||||
}, [ide])
|
}, [ide])
|
||||||
|
|
||||||
const _goToCodeLine = useCallback(
|
const goToCodeLine = useCallback(
|
||||||
(file, line) => {
|
(file, line) => {
|
||||||
if (file) {
|
if (file) {
|
||||||
const doc = ide.fileTreeManager.findEntityByPath(file)
|
const doc = ide.fileTreeManager.findEntityByPath(file)
|
||||||
|
@ -200,14 +187,7 @@ function PdfSynctexControls() {
|
||||||
[ide, isMounted, setSynctexError]
|
[ide, isMounted, setSynctexError]
|
||||||
)
|
)
|
||||||
|
|
||||||
const goToCodeLine = useDetachAction(
|
const goToPdfLocation = useCallback(
|
||||||
'go-to-code-line',
|
|
||||||
_goToCodeLine,
|
|
||||||
'detached',
|
|
||||||
'detacher'
|
|
||||||
)
|
|
||||||
|
|
||||||
const _goToPdfLocation = useCallback(
|
|
||||||
params => {
|
params => {
|
||||||
setSyncToPdfInFlight(true)
|
setSyncToPdfInFlight(true)
|
||||||
|
|
||||||
|
@ -240,13 +220,6 @@ function PdfSynctexControls() {
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const goToPdfLocation = useDetachAction(
|
|
||||||
'go-to-pdf-location',
|
|
||||||
_goToPdfLocation,
|
|
||||||
'detacher',
|
|
||||||
'detached'
|
|
||||||
)
|
|
||||||
|
|
||||||
const syncToPdf = useCallback(
|
const syncToPdf = useCallback(
|
||||||
cursorPosition => {
|
cursorPosition => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
@ -260,7 +233,7 @@ function PdfSynctexControls() {
|
||||||
[getCurrentFilePath, goToPdfLocation]
|
[getCurrentFilePath, goToPdfLocation]
|
||||||
)
|
)
|
||||||
|
|
||||||
const syncToCode = useCallback(
|
const _syncToCode = useCallback(
|
||||||
(position, visualOffset = 0) => {
|
(position, visualOffset = 0) => {
|
||||||
setSyncToCodeInFlight(true)
|
setSyncToCodeInFlight(true)
|
||||||
// FIXME: this actually works better if it's halfway across the
|
// 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(() => {
|
useEffect(() => {
|
||||||
const listener = event => syncToCode(event.detail)
|
const listener = event => syncToCode(event.detail)
|
||||||
window.addEventListener('synctex:sync-to-position', listener)
|
window.addEventListener('synctex:sync-to-position', listener)
|
||||||
|
@ -325,22 +305,32 @@ function PdfSynctexControls() {
|
||||||
}
|
}
|
||||||
}, [syncToCode])
|
}, [syncToCode])
|
||||||
|
|
||||||
const hasSingleSelectedDoc = useMemo(() => {
|
const [hasSingleSelectedDoc, setHasSingleSelectedDoc] = useDetachState(
|
||||||
|
'has-single-selected-doc',
|
||||||
|
false,
|
||||||
|
'detacher',
|
||||||
|
'detached'
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (selectedEntities.length !== 1) {
|
if (selectedEntities.length !== 1) {
|
||||||
return false
|
setHasSingleSelectedDoc(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedEntities[0].type !== 'doc') {
|
if (selectedEntities[0].type !== 'doc') {
|
||||||
return false
|
setHasSingleSelectedDoc(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}, [selectedEntities])
|
setHasSingleSelectedDoc(true)
|
||||||
|
}, [selectedEntities, setHasSingleSelectedDoc])
|
||||||
|
|
||||||
if (!position) {
|
if (!position) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pdfExists || pdfViewer === 'native') {
|
if (!pdfUrl || pdfViewer === 'native') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { lazy, memo } from 'react'
|
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(() =>
|
const PdfJsViewer = lazy(() =>
|
||||||
import(/* webpackChunkName: "pdf-js-viewer" */ './pdf-js-viewer')
|
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 getMeta from '../../../utils/meta'
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||||
import { useDetachContext } from '../../../shared/context/detach-context'
|
|
||||||
import useEventListener from '../../../shared/hooks/use-event-listener'
|
import useEventListener from '../../../shared/hooks/use-event-listener'
|
||||||
import useDetachAction from '../../../shared/hooks/use-detach-action'
|
import useDetachAction from '../../../shared/hooks/use-detach-action'
|
||||||
import usePreviousValue from '../../../shared/hooks/use-previous-value'
|
|
||||||
|
|
||||||
const showPdfDetach = getMeta('ol-showPdfDetach')
|
const showPdfDetach = getMeta('ol-showPdfDetach')
|
||||||
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
|
||||||
|
|
||||||
export default function useCompileTriggers() {
|
export default function useCompileTriggers() {
|
||||||
const { startCompile, setChangedAt, cleanupCompileResult, setError } =
|
const { startCompile, setChangedAt } = useCompileContext()
|
||||||
useCompileContext()
|
|
||||||
const { role: detachRole } = useDetachContext()
|
|
||||||
|
|
||||||
// recompile on key press
|
// recompile on key press
|
||||||
const startOrTriggerCompile = useDetachAction(
|
const startOrTriggerCompile = useDetachAction(
|
||||||
|
@ -43,23 +38,4 @@ export default function useCompileTriggers() {
|
||||||
}, [setOrTriggerChangedAt, setChangedAt])
|
}, [setOrTriggerChangedAt, setChangedAt])
|
||||||
useEventListener('doc:changed', setChangedAtHandler)
|
useEventListener('doc:changed', setChangedAtHandler)
|
||||||
useEventListener('doc:saved', 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
|
let type
|
||||||
if (ruleDetails.ruleId != null) {
|
if (ruleDetails.ruleId != null) {
|
||||||
entry.ruleId = ruleDetails.ruleId
|
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) {
|
if (ruleDetails.newMessage != null) {
|
||||||
entry.message = entry.message.replace(
|
entry.message = entry.message.replace(
|
||||||
|
@ -54,19 +49,6 @@ export default {
|
||||||
seenErrorTypes[type] = true
|
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 */
|
/* 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 = [
|
const rules = [
|
||||||
{
|
{
|
||||||
|
ruleId: 'hint_misplaced_alignment_tab_character',
|
||||||
regexToMatch: /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/,
|
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 \$\$/,
|
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./,
|
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/,
|
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/,
|
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 .+/,
|
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)?/,
|
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'/,
|
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/,
|
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/,
|
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/,
|
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: \..+/,
|
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'/,
|
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'/,
|
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/,
|
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',
|
ruleId: 'hint_mismatched_environment',
|
||||||
types: ['environment'],
|
types: ['environment'],
|
||||||
regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/,
|
regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/,
|
||||||
newMessage: 'Error: environment does not match \\begin{$1} ... \\end{$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',
|
ruleId: 'hint_mismatched_brackets',
|
||||||
types: ['environment'],
|
types: ['environment'],
|
||||||
regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/,
|
regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/,
|
||||||
newMessage: "Error: brackets do not match, found '$2' instead of '$1'",
|
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/,
|
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/,
|
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/,
|
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/,
|
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/,
|
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/,
|
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/,
|
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/,
|
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/,
|
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/,
|
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)/,
|
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/,
|
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}/,
|
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',
|
ruleId: 'hint_mismatched_environment2',
|
||||||
|
@ -518,15 +136,6 @@ const rules = [
|
||||||
regexToMatch:
|
regexToMatch:
|
||||||
/Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/,
|
/Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/,
|
||||||
newMessage: 'Error: environments do not match: \\begin{$1} ... \\end{$2}',
|
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',
|
ruleId: 'hint_mismatched_environment3',
|
||||||
|
@ -535,15 +144,6 @@ const rules = [
|
||||||
regexToMatch:
|
regexToMatch:
|
||||||
/Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/,
|
/Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/,
|
||||||
newMessage: 'Warning: No matching \\end found for \\begin{$1}',
|
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',
|
ruleId: 'hint_mismatched_environment4',
|
||||||
|
@ -552,29 +152,7 @@ const rules = [
|
||||||
regexToMatch:
|
regexToMatch:
|
||||||
/Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/,
|
/Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/,
|
||||||
newMessage: 'Error: found \\end{$1} without a corresponding \\begin{$1}',
|
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
|
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')}`
|
const SYSEND_CHANNEL = `detach-${getMeta('ol-project_id')}`
|
||||||
|
|
||||||
export function DetachProvider({ children }) {
|
export function DetachProvider({ children }) {
|
||||||
|
const [lastDetachedConnectedAt, setLastDetachedConnectedAt] = useState()
|
||||||
const [role, setRole] = useState(() => getMeta('ol-detachRole') || null)
|
const [role, setRole] = useState(() => getMeta('ol-detachRole') || null)
|
||||||
const {
|
const {
|
||||||
addHandler: addEventHandler,
|
addHandler: addEventHandler,
|
||||||
|
@ -94,15 +95,33 @@ export function DetachProvider({ children }) {
|
||||||
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
||||||
}, [broadcastEvent])
|
}, [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(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
role,
|
role,
|
||||||
setRole,
|
setRole,
|
||||||
broadcastEvent,
|
broadcastEvent,
|
||||||
|
lastDetachedConnectedAt,
|
||||||
addEventHandler,
|
addEventHandler,
|
||||||
deleteEventHandler,
|
deleteEventHandler,
|
||||||
}),
|
}),
|
||||||
[role, setRole, broadcastEvent, addEventHandler, deleteEventHandler]
|
[
|
||||||
|
role,
|
||||||
|
setRole,
|
||||||
|
broadcastEvent,
|
||||||
|
lastDetachedConnectedAt,
|
||||||
|
addEventHandler,
|
||||||
|
deleteEventHandler,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -155,7 +155,7 @@ export function EditorProvider({ children, settings }) {
|
||||||
|
|
||||||
EditorProvider.propTypes = {
|
EditorProvider.propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
settings: PropTypes.any.isRequired,
|
settings: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEditorContext(propTypes) {
|
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 PropTypes from 'prop-types'
|
||||||
|
import { getMockIde } from './mock/mock-ide'
|
||||||
|
|
||||||
const IdeContext = createContext()
|
const IdeContext = createContext()
|
||||||
|
|
||||||
|
@ -20,11 +21,13 @@ export function useIdeContext() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IdeProvider({ ide, children }) {
|
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 = {
|
IdeProvider.propTypes = {
|
||||||
children: PropTypes.any.isRequired,
|
children: PropTypes.any.isRequired,
|
||||||
ide: PropTypes.shape({
|
ide: PropTypes.shape({
|
||||||
$scope: PropTypes.object.isRequired,
|
$scope: PropTypes.object.isRequired,
|
||||||
}).isRequired,
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,11 +108,13 @@ export function LayoutProvider({ children }) {
|
||||||
isLinking: detachIsLinking,
|
isLinking: detachIsLinking,
|
||||||
isLinked: detachIsLinked,
|
isLinked: detachIsLinked,
|
||||||
role: detachRole,
|
role: detachRole,
|
||||||
|
isRedundant: detachIsRedundant,
|
||||||
} = useDetachLayout()
|
} = useDetachLayout()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debugPdfDetach) {
|
if (debugPdfDetach) {
|
||||||
console.log('Layout Effect', {
|
console.log('Layout Effect', {
|
||||||
|
detachIsRedundant,
|
||||||
detachRole,
|
detachRole,
|
||||||
detachIsLinking,
|
detachIsLinking,
|
||||||
detachIsLinked,
|
detachIsLinked,
|
||||||
|
@ -121,12 +123,23 @@ export function LayoutProvider({ children }) {
|
||||||
|
|
||||||
if (detachRole !== 'detacher') return // not in a PDF detacher layout
|
if (detachRole !== 'detacher') return // not in a PDF detacher layout
|
||||||
|
|
||||||
|
if (detachIsRedundant) {
|
||||||
|
changeLayout('sideBySide')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (detachIsLinking || detachIsLinked) {
|
if (detachIsLinking || detachIsLinked) {
|
||||||
// the tab is linked to a detached tab (or about to be linked); show
|
// the tab is linked to a detached tab (or about to be linked); show
|
||||||
// editor only
|
// editor only
|
||||||
changeLayout('flat', 'editor')
|
changeLayout('flat', 'editor')
|
||||||
}
|
}
|
||||||
}, [detachRole, detachIsLinking, detachIsLinked, changeLayout])
|
}, [
|
||||||
|
detachIsRedundant,
|
||||||
|
detachRole,
|
||||||
|
detachIsLinking,
|
||||||
|
detachIsLinked,
|
||||||
|
changeLayout,
|
||||||
|
])
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
@ -13,7 +13,11 @@ import useScopeValueSetterOnly from '../hooks/use-scope-value-setter-only'
|
||||||
import usePersistedState from '../hooks/use-persisted-state'
|
import usePersistedState from '../hooks/use-persisted-state'
|
||||||
import useAbortController from '../hooks/use-abort-controller'
|
import useAbortController from '../hooks/use-abort-controller'
|
||||||
import DocumentCompiler from '../../features/pdf-preview/util/compiler'
|
import DocumentCompiler from '../../features/pdf-preview/util/compiler'
|
||||||
import { send, sendMBSampled } from '../../infrastructure/event-tracking'
|
import {
|
||||||
|
send,
|
||||||
|
sendMBOnce,
|
||||||
|
sendMBSampled,
|
||||||
|
} from '../../infrastructure/event-tracking'
|
||||||
import {
|
import {
|
||||||
buildLogEntryAnnotations,
|
buildLogEntryAnnotations,
|
||||||
handleLogFiles,
|
handleLogFiles,
|
||||||
|
@ -24,9 +28,9 @@ import { useProjectContext } from './project-context'
|
||||||
import { useEditorContext } from './editor-context'
|
import { useEditorContext } from './editor-context'
|
||||||
import { buildFileList } from '../../features/pdf-preview/util/file-list'
|
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({
|
value: PropTypes.shape({
|
||||||
autoCompile: PropTypes.bool.isRequired,
|
autoCompile: PropTypes.bool.isRequired,
|
||||||
clearingCache: PropTypes.bool.isRequired,
|
clearingCache: PropTypes.bool.isRequired,
|
||||||
|
@ -52,6 +56,7 @@ CompileContext.Provider.propTypes = {
|
||||||
setHighlights: PropTypes.func.isRequired,
|
setHighlights: PropTypes.func.isRequired,
|
||||||
setPosition: PropTypes.func.isRequired,
|
setPosition: PropTypes.func.isRequired,
|
||||||
setShowLogs: PropTypes.func.isRequired,
|
setShowLogs: PropTypes.func.isRequired,
|
||||||
|
toggleLogs: PropTypes.func.isRequired,
|
||||||
setStopOnValidationError: PropTypes.func.isRequired,
|
setStopOnValidationError: PropTypes.func.isRequired,
|
||||||
showLogs: PropTypes.bool.isRequired,
|
showLogs: PropTypes.bool.isRequired,
|
||||||
stopOnValidationError: 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 ide = useIdeContext()
|
||||||
|
|
||||||
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
|
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
|
||||||
|
@ -111,6 +118,15 @@ export function CompileProvider({ children }) {
|
||||||
// whether the logs should be visible
|
// whether the logs should be visible
|
||||||
const [showLogs, setShowLogs] = useState(false)
|
const [showLogs, setShowLogs] = useState(false)
|
||||||
|
|
||||||
|
const toggleLogs = useCallback(() => {
|
||||||
|
setShowLogs(prev => {
|
||||||
|
if (!prev) {
|
||||||
|
sendMBOnce('ide-open-logs-once')
|
||||||
|
}
|
||||||
|
return !prev
|
||||||
|
})
|
||||||
|
}, [setShowLogs])
|
||||||
|
|
||||||
// an error that occurred
|
// an error that occurred
|
||||||
const [error, setError] = useState()
|
const [error, setError] = useState()
|
||||||
|
|
||||||
|
@ -445,6 +461,7 @@ export function CompileProvider({ children }) {
|
||||||
setHighlights,
|
setHighlights,
|
||||||
setPosition,
|
setPosition,
|
||||||
setShowLogs,
|
setShowLogs,
|
||||||
|
toggleLogs,
|
||||||
setStopOnValidationError,
|
setStopOnValidationError,
|
||||||
showLogs,
|
showLogs,
|
||||||
startCompile,
|
startCompile,
|
||||||
|
@ -492,20 +509,29 @@ export function CompileProvider({ children }) {
|
||||||
firstRenderDone,
|
firstRenderDone,
|
||||||
setChangedAt,
|
setChangedAt,
|
||||||
cleanupCompileResult,
|
cleanupCompileResult,
|
||||||
|
setShowLogs,
|
||||||
|
toggleLogs,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CompileContext.Provider value={value}>{children}</CompileContext.Provider>
|
<LocalCompileContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</LocalCompileContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
CompileProvider.propTypes = {
|
LocalCompileProvider.propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCompileContext(propTypes) {
|
export function useLocalCompileContext(propTypes) {
|
||||||
const data = useContext(CompileContext)
|
const data = useContext(LocalCompileContext)
|
||||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'CompileContext.Provider')
|
PropTypes.checkPropTypes(
|
||||||
|
propTypes,
|
||||||
|
data,
|
||||||
|
'data',
|
||||||
|
'LocalCompileContext.Provider'
|
||||||
|
)
|
||||||
return data
|
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 { UserProvider } from './user-context'
|
||||||
import { IdeProvider } from './ide-context'
|
import { IdeProvider } from './ide-context'
|
||||||
import { EditorProvider } from './editor-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 { LayoutProvider } from './layout-context'
|
||||||
import { DetachProvider } from './detach-context'
|
import { DetachProvider } from './detach-context'
|
||||||
import { ChatProvider } from '../../features/chat/context/chat-context'
|
import { ChatProvider } from '../../features/chat/context/chat-context'
|
||||||
|
@ -22,9 +23,11 @@ export function ContextRoot({ children, ide, settings }) {
|
||||||
<EditorProvider settings={settings}>
|
<EditorProvider settings={settings}>
|
||||||
<DetachProvider>
|
<DetachProvider>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<CompileProvider>
|
<LocalCompileProvider>
|
||||||
<ChatProvider>{children}</ChatProvider>
|
<DetachCompileProvider>
|
||||||
</CompileProvider>
|
<ChatProvider>{children}</ChatProvider>
|
||||||
|
</DetachCompileProvider>
|
||||||
|
</LocalCompileProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</DetachProvider>
|
</DetachProvider>
|
||||||
</EditorProvider>
|
</EditorProvider>
|
||||||
|
@ -38,8 +41,8 @@ export function ContextRoot({ children, ide, settings }) {
|
||||||
|
|
||||||
ContextRoot.propTypes = {
|
ContextRoot.propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
ide: PropTypes.any.isRequired,
|
ide: PropTypes.object,
|
||||||
settings: PropTypes.any.isRequired,
|
settings: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rootContext = createSharedContext(ContextRoot)
|
export const rootContext = createSharedContext(ContextRoot)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createContext, useContext } from 'react'
|
import { createContext, useContext } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import useScopeValue from '../hooks/use-scope-value'
|
import getMeta from '../../utils/meta'
|
||||||
|
|
||||||
export const UserContext = createContext()
|
export const UserContext = createContext()
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ UserContext.Provider.propTypes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserProvider({ children }) {
|
export function UserProvider({ children }) {
|
||||||
const [user] = useScopeValue('user', true)
|
const user = getMeta('ol-user')
|
||||||
|
|
||||||
return <UserContext.Provider value={user}>{children}</UserContext.Provider>
|
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() {
|
export default function useCallbackHandlers() {
|
||||||
const [handlers, setHandlers] = useState(new Set())
|
const handlersRef = useRef(new Set())
|
||||||
|
|
||||||
const addHandler = useCallback(
|
const addHandler = useCallback(handler => {
|
||||||
handler => {
|
handlersRef.current.add(handler)
|
||||||
setHandlers(prev => new Set(prev.add(handler)))
|
}, [])
|
||||||
},
|
|
||||||
[setHandlers]
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteHandler = useCallback(
|
const deleteHandler = useCallback(handler => {
|
||||||
handler => {
|
handlersRef.current.delete(handler)
|
||||||
setHandlers(prev => {
|
}, [])
|
||||||
prev.delete(handler)
|
|
||||||
return new Set(prev)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[setHandlers]
|
|
||||||
)
|
|
||||||
|
|
||||||
const callHandlers = useCallback(
|
const callHandlers = useCallback((...args) => {
|
||||||
(...args) => {
|
for (const handler of handlersRef.current) {
|
||||||
for (const handler of handlers) {
|
handler(...args)
|
||||||
handler(...args)
|
}
|
||||||
}
|
}, [])
|
||||||
},
|
|
||||||
[handlers]
|
|
||||||
)
|
|
||||||
|
|
||||||
return { addHandler, deleteHandler, callHandlers }
|
return { addHandler, deleteHandler, callHandlers }
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,10 @@ export default function useDetachLayout() {
|
||||||
// isLinked: when the tab is linked to another tab (of different role)
|
// isLinked: when the tab is linked to another tab (of different role)
|
||||||
const [isLinked, setIsLinked] = useState(false)
|
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()
|
const uiTimeoutRef = useRef()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -76,12 +80,24 @@ export default function useDetachLayout() {
|
||||||
}, [setRole, setIsLinked, broadcastEvent])
|
}, [setRole, setIsLinked, broadcastEvent])
|
||||||
|
|
||||||
const detach = useCallback(() => {
|
const detach = useCallback(() => {
|
||||||
|
setIsRedundant(false)
|
||||||
setRole('detacher')
|
setRole('detacher')
|
||||||
setIsLinking(true)
|
setIsLinking(true)
|
||||||
|
|
||||||
window.open(buildUrlWithDetachRole('detached').toString(), '_blank')
|
window.open(buildUrlWithDetachRole('detached').toString(), '_blank')
|
||||||
}, [setRole, setIsLinking])
|
}, [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(
|
const handleEventForDetacherFromDetached = useCallback(
|
||||||
message => {
|
message => {
|
||||||
switch (message.event) {
|
switch (message.event) {
|
||||||
|
@ -122,7 +138,7 @@ export default function useDetachLayout() {
|
||||||
[setIsLinked, broadcastEvent]
|
[setIsLinked, broadcastEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEventFromSelf = useCallback(
|
const handleEventForDetachedFromDetached = useCallback(
|
||||||
message => {
|
message => {
|
||||||
switch (message.event) {
|
switch (message.event) {
|
||||||
case 'closed':
|
case 'closed':
|
||||||
|
@ -137,7 +153,7 @@ export default function useDetachLayout() {
|
||||||
message => {
|
message => {
|
||||||
if (role === 'detacher') {
|
if (role === 'detacher') {
|
||||||
if (message.role === 'detacher') {
|
if (message.role === 'detacher') {
|
||||||
handleEventFromSelf(message)
|
handleEventForDetacherFromDetacher(message)
|
||||||
} else if (message.role === 'detached') {
|
} else if (message.role === 'detached') {
|
||||||
handleEventForDetacherFromDetached(message)
|
handleEventForDetacherFromDetached(message)
|
||||||
}
|
}
|
||||||
|
@ -145,15 +161,16 @@ export default function useDetachLayout() {
|
||||||
if (message.role === 'detacher') {
|
if (message.role === 'detacher') {
|
||||||
handleEventForDetachedFromDetacher(message)
|
handleEventForDetachedFromDetacher(message)
|
||||||
} else if (message.role === 'detached') {
|
} else if (message.role === 'detached') {
|
||||||
handleEventFromSelf(message)
|
handleEventForDetachedFromDetached(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
role,
|
role,
|
||||||
|
handleEventForDetacherFromDetacher,
|
||||||
handleEventForDetacherFromDetached,
|
handleEventForDetacherFromDetached,
|
||||||
handleEventForDetachedFromDetacher,
|
handleEventForDetachedFromDetacher,
|
||||||
handleEventFromSelf,
|
handleEventForDetachedFromDetached,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -168,5 +185,6 @@ export default function useDetachLayout() {
|
||||||
isLinked,
|
isLinked,
|
||||||
isLinking,
|
isLinking,
|
||||||
role,
|
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 [value, setValue] = useState(defaultValue)
|
||||||
|
|
||||||
const { role, broadcastEvent, addEventHandler, deleteEventHandler } =
|
const {
|
||||||
useDetachContext()
|
role,
|
||||||
|
broadcastEvent,
|
||||||
|
lastDetachedConnectedAt,
|
||||||
|
addEventHandler,
|
||||||
|
deleteEventHandler,
|
||||||
|
} = useDetachContext()
|
||||||
|
|
||||||
const eventName = `state-${key}`
|
const eventName = `state-${key}`
|
||||||
|
|
||||||
|
// lastDetachedConnectedAt is added as a dependency in order to re-broadcast
|
||||||
|
// all states when a new detached tab connects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (role === senderRole) {
|
if (role === senderRole) {
|
||||||
broadcastEvent(eventName, { value })
|
broadcastEvent(eventName, { value })
|
||||||
}
|
}
|
||||||
}, [role, senderRole, eventName, value, broadcastEvent])
|
}, [
|
||||||
|
role,
|
||||||
|
senderRole,
|
||||||
|
eventName,
|
||||||
|
value,
|
||||||
|
broadcastEvent,
|
||||||
|
lastDetachedConnectedAt,
|
||||||
|
])
|
||||||
|
|
||||||
const handleStateEvent = useCallback(
|
const handleStateEvent = useCallback(
|
||||||
message => {
|
message => {
|
||||||
|
|
|
@ -37,7 +37,7 @@ function usePersistedState(key, defaultValue, listen = false) {
|
||||||
if (event.key === key) {
|
if (event.key === key) {
|
||||||
// note: this value is read via getItem rather than from event.newValue
|
// note: this value is read via getItem rather than from event.newValue
|
||||||
// because getItem handles deserializing the JSON that's stored in localStorage.
|
// 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)
|
window.removeEventListener('storage', listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [key, listen])
|
}, [key, listen, defaultValue])
|
||||||
|
|
||||||
return [value, updateFunction]
|
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 PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
|
||||||
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
|
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
|
||||||
import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
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 {
|
import {
|
||||||
dispatchDocChanged,
|
dispatchDocChanged,
|
||||||
mockBuildFile,
|
mockBuildFile,
|
||||||
|
|
|
@ -1580,6 +1580,7 @@
|
||||||
"project_layout_sharing_submission": "Project Layout, Sharing, and Submission",
|
"project_layout_sharing_submission": "Project Layout, Sharing, and Submission",
|
||||||
"pdf_in_separate_tab": "PDF in separate tab",
|
"pdf_in_separate_tab": "PDF in separate tab",
|
||||||
"tab_no_longer_connected": "This tab is no longer connected with the editor",
|
"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",
|
"redirect_to_editor": "Redirect to editor",
|
||||||
"layout_processing": "Layout processing",
|
"layout_processing": "Layout processing",
|
||||||
"show_in_code": "Show in code",
|
"show_in_code": "Show in code",
|
||||||
|
|
|
@ -15,24 +15,27 @@ import { stubMathJax, tearDownMathJaxStubs } from './stubs'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
|
|
||||||
describe('<ChatPane />', function () {
|
describe('<ChatPane />', function () {
|
||||||
|
const user = {
|
||||||
|
id: 'fake_user',
|
||||||
|
first_name: 'fake_user_first_name',
|
||||||
|
email: 'fake@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.clock = sinon.useFakeTimers({
|
this.clock = sinon.useFakeTimers({
|
||||||
toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'],
|
toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'],
|
||||||
})
|
})
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', user)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
this.clock.runAll()
|
this.clock.runAll()
|
||||||
this.clock.restore()
|
this.clock.restore()
|
||||||
fetchMock.reset()
|
fetchMock.reset()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = {
|
|
||||||
id: 'fake_user',
|
|
||||||
first_name: 'fake_user_first_name',
|
|
||||||
email: 'fake@example.com',
|
|
||||||
}
|
|
||||||
|
|
||||||
const testMessages = [
|
const testMessages = [
|
||||||
{
|
{
|
||||||
id: 'msg_1',
|
id: 'msg_1',
|
||||||
|
|
|
@ -25,10 +25,15 @@ describe('ChatContext', function () {
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
|
|
||||||
stubMathJax()
|
stubMathJax()
|
||||||
|
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', user)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
tearDownMathJaxStubs()
|
tearDownMathJaxStubs()
|
||||||
|
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('socket connection', function () {
|
describe('socket connection', function () {
|
||||||
|
@ -42,7 +47,7 @@ describe('ChatContext', function () {
|
||||||
|
|
||||||
it('subscribes when mounted', function () {
|
it('subscribes when mounted', function () {
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
renderChatContextHook({ user, socket })
|
renderChatContextHook({ socket })
|
||||||
|
|
||||||
// Assert that there is 1 listener
|
// Assert that there is 1 listener
|
||||||
expect(socket.rawListeners('new-chat-message').length).to.equal(1)
|
expect(socket.rawListeners('new-chat-message').length).to.equal(1)
|
||||||
|
@ -50,7 +55,7 @@ describe('ChatContext', function () {
|
||||||
|
|
||||||
it('unsubscribes when unmounted', function () {
|
it('unsubscribes when unmounted', function () {
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
const { unmount } = renderChatContextHook({ user, socket })
|
const { unmount } = renderChatContextHook({ socket })
|
||||||
|
|
||||||
unmount()
|
unmount()
|
||||||
|
|
||||||
|
@ -62,7 +67,6 @@ describe('ChatContext', function () {
|
||||||
// Mock socket: we only need to emit events, not mock actual connections
|
// Mock socket: we only need to emit events, not mock actual connections
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||||
user,
|
|
||||||
socket,
|
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 () {
|
it("doesn't add received messages from the current user if a message was just sent", async function () {
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||||
user,
|
|
||||||
socket,
|
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 () {
|
it('adds the new message from the current user if another message was received after sending', async function () {
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||||
user,
|
|
||||||
socket,
|
socket,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -187,7 +189,7 @@ describe('ChatContext', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds messages to the list', async function () {
|
it('adds messages to the list', async function () {
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.loadInitialMessages()
|
result.current.loadInitialMessages()
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -196,7 +198,7 @@ describe('ChatContext', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("won't load messages a second time", async function () {
|
it("won't load messages a second time", async function () {
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.loadInitialMessages()
|
result.current.loadInitialMessages()
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -211,7 +213,7 @@ describe('ChatContext', function () {
|
||||||
it('provides an error on failure', async function () {
|
it('provides an error on failure', async function () {
|
||||||
fetchMock.reset()
|
fetchMock.reset()
|
||||||
fetchMock.get('express:/project/:projectId/messages', 500)
|
fetchMock.get('express:/project/:projectId/messages', 500)
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.loadInitialMessages()
|
result.current.loadInitialMessages()
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -233,7 +235,7 @@ describe('ChatContext', function () {
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.loadMoreMessages()
|
result.current.loadMoreMessages()
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -267,7 +269,7 @@ describe('ChatContext', function () {
|
||||||
{ overwriteRoutes: false }
|
{ overwriteRoutes: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.loadMoreMessages()
|
result.current.loadMoreMessages()
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -297,7 +299,7 @@ describe('ChatContext', function () {
|
||||||
createMessages(49, user)
|
createMessages(49, user)
|
||||||
)
|
)
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.loadMoreMessages()
|
result.current.loadMoreMessages()
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -322,7 +324,6 @@ describe('ChatContext', function () {
|
||||||
|
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({
|
const { result, waitForNextUpdate } = renderChatContextHook({
|
||||||
user,
|
|
||||||
socket,
|
socket,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -367,7 +368,7 @@ describe('ChatContext', function () {
|
||||||
it('provides an error on failures', async function () {
|
it('provides an error on failures', async function () {
|
||||||
fetchMock.reset()
|
fetchMock.reset()
|
||||||
fetchMock.get('express:/project/:projectId/messages', 500)
|
fetchMock.get('express:/project/:projectId/messages', 500)
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.loadMoreMessages()
|
result.current.loadMoreMessages()
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -387,7 +388,7 @@ describe('ChatContext', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('optimistically adds the message to the list', function () {
|
it('optimistically adds the message to the list', function () {
|
||||||
const { result } = renderChatContextHook({ user })
|
const { result } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.sendMessage('sent message')
|
result.current.sendMessage('sent message')
|
||||||
|
|
||||||
|
@ -397,7 +398,7 @@ describe('ChatContext', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('POSTs the message to the backend', function () {
|
it('POSTs the message to the backend', function () {
|
||||||
const { result } = renderChatContextHook({ user })
|
const { result } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.sendMessage('sent message')
|
result.current.sendMessage('sent message')
|
||||||
|
|
||||||
|
@ -409,7 +410,7 @@ describe('ChatContext', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("doesn't send if the content is empty", function () {
|
it("doesn't send if the content is empty", function () {
|
||||||
const { result } = renderChatContextHook({ user })
|
const { result } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.sendMessage('')
|
result.current.sendMessage('')
|
||||||
|
|
||||||
|
@ -426,7 +427,7 @@ describe('ChatContext', function () {
|
||||||
fetchMock
|
fetchMock
|
||||||
.get('express:/project/:projectId/messages', [])
|
.get('express:/project/:projectId/messages', [])
|
||||||
.postOnce('express:/project/:projectId/messages', 500)
|
.postOnce('express:/project/:projectId/messages', 500)
|
||||||
const { result, waitForNextUpdate } = renderChatContextHook({ user })
|
const { result, waitForNextUpdate } = renderChatContextHook({})
|
||||||
|
|
||||||
result.current.sendMessage('sent message')
|
result.current.sendMessage('sent message')
|
||||||
await waitForNextUpdate()
|
await waitForNextUpdate()
|
||||||
|
@ -444,7 +445,7 @@ describe('ChatContext', function () {
|
||||||
|
|
||||||
it('increments unreadMessageCount when a new message is received', function () {
|
it('increments unreadMessageCount when a new message is received', function () {
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
const { result } = renderChatContextHook({ user, socket })
|
const { result } = renderChatContextHook({ socket })
|
||||||
|
|
||||||
// Receive a new message from the socket
|
// Receive a new message from the socket
|
||||||
socket.emit('new-chat-message', {
|
socket.emit('new-chat-message', {
|
||||||
|
@ -459,7 +460,7 @@ describe('ChatContext', function () {
|
||||||
|
|
||||||
it('resets unreadMessageCount when markMessagesAsRead is called', function () {
|
it('resets unreadMessageCount when markMessagesAsRead is called', function () {
|
||||||
const socket = new EventEmitter()
|
const socket = new EventEmitter()
|
||||||
const { result } = renderChatContextHook({ user, socket })
|
const { result } = renderChatContextHook({ socket })
|
||||||
|
|
||||||
// Receive a new message from the socket, incrementing unreadMessageCount
|
// Receive a new message from the socket, incrementing unreadMessageCount
|
||||||
// by 1
|
// by 1
|
||||||
|
|
|
@ -18,7 +18,6 @@ describe('<LayoutDropdownButton />', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
openStub = sinon.stub(window, 'open')
|
openStub = sinon.stub(window, 'open')
|
||||||
window.metaAttributesCache = new Map()
|
window.metaAttributesCache = new Map()
|
||||||
fetchMock.post('express:/project/:projectId/compile/stop', () => 204)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -101,11 +100,6 @@ describe('<LayoutDropdownButton />', function () {
|
||||||
screen.getByText('Layout processing')
|
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 () {
|
it('should record event', function () {
|
||||||
sinon.assert.calledWith(eventTrackingSpy.sendMB, 'project-layout-detach')
|
sinon.assert.calledWith(eventTrackingSpy.sendMB, 'project-layout-detach')
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,8 @@ describe('<FileTreeRoot/>', function () {
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
global.requestAnimationFrame = sinon.stub()
|
global.requestAnimationFrame = sinon.stub()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -24,6 +26,7 @@ describe('<FileTreeRoot/>', function () {
|
||||||
onInit.reset()
|
onInit.reset()
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
global.localStorage.clear()
|
global.localStorage.clear()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders', function () {
|
it('renders', function () {
|
||||||
|
|
|
@ -12,10 +12,16 @@ describe('FileTree Context Menu Flow', function () {
|
||||||
const onSelect = sinon.stub()
|
const onSelect = sinon.stub()
|
||||||
const onInit = sinon.stub()
|
const onInit = sinon.stub()
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
onSelect.reset()
|
onSelect.reset()
|
||||||
onInit.reset()
|
onInit.reset()
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens on contextMenu event', async function () {
|
it('opens on contextMenu event', async function () {
|
||||||
|
|
|
@ -16,6 +16,8 @@ describe('FileTree Create Folder Flow', function () {
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
global.requestAnimationFrame = sinon.stub()
|
global.requestAnimationFrame = sinon.stub()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -24,6 +26,7 @@ describe('FileTree Create Folder Flow', function () {
|
||||||
onSelect.reset()
|
onSelect.reset()
|
||||||
onInit.reset()
|
onInit.reset()
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('add to root when no files are selected', async function () {
|
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 onSelect = sinon.stub()
|
||||||
const onInit = sinon.stub()
|
const onInit = sinon.stub()
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
fetchMock.restore()
|
fetchMock.restore()
|
||||||
onSelect.reset()
|
onSelect.reset()
|
||||||
onInit.reset()
|
onInit.reset()
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('single entity', function () {
|
describe('single entity', function () {
|
||||||
|
|
|
@ -16,6 +16,8 @@ describe('FileTree Rename Entity Flow', function () {
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
global.requestAnimationFrame = sinon.stub()
|
global.requestAnimationFrame = sinon.stub()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -24,6 +26,7 @@ describe('FileTree Rename Entity Flow', function () {
|
||||||
onSelect.reset()
|
onSelect.reset()
|
||||||
onInit.reset()
|
onInit.reset()
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import DetachCompileButton from '../../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
|
import DetachCompileButton from '../../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
|
||||||
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
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 sysendTestHelper from '../../../helpers/sysend'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
@ -48,23 +48,4 @@ describe('<DetachCompileButton/>', function () {
|
||||||
})
|
})
|
||||||
).to.not.exist
|
).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',
|
message: 'LaTeX Error',
|
||||||
content: 'See the LaTeX manual',
|
content: 'See the LaTeX manual',
|
||||||
raw: '',
|
raw: '',
|
||||||
ruleId: 'latex_error',
|
ruleId: 'hint_misplaced_alignment_tab_character',
|
||||||
humanReadableHint: '',
|
|
||||||
humanReadableHintComponent: <></>,
|
|
||||||
key: '',
|
key: '',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -36,6 +34,14 @@ describe('<PdfLogsEntries/>', function () {
|
||||||
fileTreeManager.findEntityByPath.resetHistory()
|
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 () {
|
it('opens doc on click', async function () {
|
||||||
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
|
renderWithEditorContext(<PdfLogsEntries entries={logEntries} />, {
|
||||||
fileTreeManager,
|
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 PdfPreviewHybridToolbar from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
||||||
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||||
import { screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
|
import sysendTestHelper from '../../../helpers/sysend'
|
||||||
|
|
||||||
describe('<PdfPreviewHybridToolbar/>', function () {
|
describe('<PdfPreviewHybridToolbar/>', function () {
|
||||||
|
let clock
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
clock = sinon.useFakeTimers()
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
window.metaAttributesCache = new Map()
|
window.metaAttributesCache = new Map()
|
||||||
|
sysendTestHelper.resetHistory()
|
||||||
|
clock.runAll()
|
||||||
|
clock.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows normal mode', async function () {
|
it('shows normal mode', async function () {
|
||||||
|
@ -15,12 +26,49 @@ describe('<PdfPreviewHybridToolbar/>', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows orphan mode', async function () {
|
describe('orphan mode', async function () {
|
||||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
it('shows connecting message on load', async function () {
|
||||||
renderWithEditorContext(<PdfPreviewHybridToolbar />)
|
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||||
|
renderWithEditorContext(<PdfPreviewHybridToolbar />)
|
||||||
|
|
||||||
await screen.getByRole('button', {
|
await screen.getByText(/Connecting with the editor/)
|
||||||
name: 'Redirect to 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
|
// click the button
|
||||||
clearCacheButton.click()
|
clearCacheButton.click()
|
||||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
await waitFor(() => {
|
||||||
|
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
||||||
})
|
})
|
||||||
|
@ -382,7 +385,7 @@ describe('<PdfPreview/>', function () {
|
||||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
expect(clearCacheButton.hasAttribute('disabled')).to.be.false
|
||||||
|
|
||||||
mockValidPdf()
|
mockValidPdf()
|
||||||
mockClearCache()
|
const finishClearCache = mockDelayed(mockClearCache)
|
||||||
|
|
||||||
const recompileFromScratch = screen.getByRole('menuitem', {
|
const recompileFromScratch = screen.getByRole('menuitem', {
|
||||||
name: 'Recompile from scratch',
|
name: 'Recompile from scratch',
|
||||||
|
@ -390,7 +393,11 @@ describe('<PdfPreview/>', function () {
|
||||||
})
|
})
|
||||||
recompileFromScratch.click()
|
recompileFromScratch.click()
|
||||||
|
|
||||||
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
await waitFor(() => {
|
||||||
|
expect(clearCacheButton.hasAttribute('disabled')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
finishClearCache()
|
||||||
|
|
||||||
// wait for compile to finish
|
// wait for compile to finish
|
||||||
await screen.findByRole('button', { name: 'Compiling…' })
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { expect } from 'chai'
|
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 { useFileTreeData } from '../../../../../frontend/js/shared/context/file-tree-data-context'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
@ -122,7 +122,6 @@ const WithSelectedEntities = ({ mockSelectedEntities = [] }) => {
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('<PdfSynctexControls/>', function () {
|
describe('<PdfSynctexControls/>', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
window.metaAttributesCache = new Map()
|
window.metaAttributesCache = new Map()
|
||||||
|
@ -185,7 +184,6 @@ describe('<PdfSynctexControls/>', function () {
|
||||||
.true
|
.true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables button when multiple entities are selected', async function () {
|
it('disables button when multiple entities are selected', async function () {
|
||||||
renderWithEditorContext(
|
renderWithEditorContext(
|
||||||
<>
|
<>
|
||||||
|
@ -236,9 +234,14 @@ describe('<PdfSynctexControls/>', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not have go to PDF location button nor arrow icon', async function () {
|
it('does not have go to PDF location button nor arrow icon', async function () {
|
||||||
const { container } = renderWithEditorContext(<PdfSynctexControls />, {
|
const { container } = renderWithEditorContext(
|
||||||
scope,
|
<>
|
||||||
})
|
<WithPosition mockPosition={mockPosition} />
|
||||||
|
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||||
|
<PdfSynctexControls />
|
||||||
|
</>,
|
||||||
|
{ scope }
|
||||||
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await screen.queryByRole('button', {
|
await screen.queryByRole('button', {
|
||||||
|
@ -249,7 +252,50 @@ describe('<PdfSynctexControls/>', function () {
|
||||||
expect(container.querySelector('.synctex-control-icon')).to.not.exist
|
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(
|
renderWithEditorContext(
|
||||||
<>
|
<>
|
||||||
<WithPosition mockPosition={mockPosition} />
|
<WithPosition mockPosition={mockPosition} />
|
||||||
|
@ -259,74 +305,23 @@ describe('<PdfSynctexControls/>', function () {
|
||||||
{ scope }
|
{ scope }
|
||||||
)
|
)
|
||||||
|
|
||||||
sysendTestHelper.resetHistory()
|
await waitFor(() => {
|
||||||
|
expect(fetchMock.called('express:/project/:projectId/compile')).to.be
|
||||||
const syncToPdfButton = await screen.findByRole('button', {
|
.true
|
||||||
name: 'Go to code location in PDF',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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({
|
sysendTestHelper.receiveMessage({
|
||||||
role: 'detached',
|
role: 'detached',
|
||||||
event: 'state-sync-to-pdf-inflight',
|
event: 'action-sync-to-code',
|
||||||
data: { value: true },
|
data: {
|
||||||
|
args: [mockPosition],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(syncToPdfButton.disabled).to.be.true
|
|
||||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
|
||||||
1
|
|
||||||
)
|
|
||||||
|
|
||||||
sysendTestHelper.receiveMessage({
|
await waitFor(() => {
|
||||||
role: 'detached',
|
expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
|
||||||
event: 'state-sync-to-pdf-inflight',
|
.true
|
||||||
data: { value: false },
|
|
||||||
})
|
})
|
||||||
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 () {
|
it('does not have go to code location button nor arrow icon', async function () {
|
||||||
const { container } = renderWithEditorContext(<PdfSynctexControls />, {
|
const { container } = renderWithEditorContext(
|
||||||
scope,
|
<>
|
||||||
})
|
<WithPosition mockPosition={mockPosition} />
|
||||||
|
<PdfSynctexControls />
|
||||||
|
</>,
|
||||||
|
{ scope }
|
||||||
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await screen.queryByRole('button', {
|
await screen.queryByRole('button', {
|
||||||
|
@ -349,102 +348,90 @@ describe('<PdfSynctexControls/>', function () {
|
||||||
expect(container.querySelector('.synctex-control-icon')).to.not.exist
|
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(
|
const { container } = renderWithEditorContext(
|
||||||
<>
|
<>
|
||||||
<WithPosition mockPosition={mockPosition} />
|
<WithPosition mockPosition={mockPosition} />
|
||||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
|
||||||
<PdfSynctexControls />
|
<PdfSynctexControls />
|
||||||
</>,
|
</>,
|
||||||
{ scope }
|
{ scope }
|
||||||
)
|
)
|
||||||
sysendTestHelper.resetHistory()
|
|
||||||
|
|
||||||
const syncToCodeButton = await screen.findByRole('button', {
|
const syncToCodeButton = await screen.findByRole('button', {
|
||||||
name: /Go to PDF location in code/,
|
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()
|
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(syncToCodeButton.disabled).to.be.false
|
||||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
fireEvent.click(syncToCodeButton)
|
sysendTestHelper.receiveMessage({
|
||||||
|
role: 'detacher',
|
||||||
|
event: 'state-sync-to-code-inflight',
|
||||||
|
data: { value: true },
|
||||||
|
})
|
||||||
|
|
||||||
expect(syncToCodeButton.disabled).to.be.true
|
expect(syncToCodeButton.disabled).to.be.true
|
||||||
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal(
|
||||||
1
|
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({
|
sysendTestHelper.receiveMessage({
|
||||||
role: 'detacher',
|
role: 'detacher',
|
||||||
event: 'action-go-to-pdf-location',
|
event: 'state-sync-to-code-inflight',
|
||||||
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',
|
|
||||||
data: { value: false },
|
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 = () =>
|
export const mockClearCache = (delayPromise = Promise.resolve()) =>
|
||||||
fetchMock.delete('express:/project/:projectId/output', 204)
|
fetchMock.delete(
|
||||||
|
'express:/project/:projectId/output',
|
||||||
|
delayPromise.then(() => ({
|
||||||
|
body: {
|
||||||
|
status: 204,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
export const mockValidPdf = () => {
|
export const mockValidPdf = () => {
|
||||||
nock('https://clsi.test-overleaf.com')
|
nock('https://clsi.test-overleaf.com')
|
||||||
|
|
|
@ -83,11 +83,14 @@ describe('<ShareProjectModal/>', function () {
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
fetchMock.get('/user/contacts', { contacts })
|
fetchMock.get('/user/contacts', { contacts })
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-user', { allowedFreeTrial: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
fetchMock.restore()
|
fetchMock.restore()
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the modal', async function () {
|
it('renders the modal', async function () {
|
||||||
|
@ -179,7 +182,7 @@ describe('<ShareProjectModal/>', function () {
|
||||||
]
|
]
|
||||||
|
|
||||||
// render as admin: actions should be present
|
// render as admin: actions should be present
|
||||||
const { rerender } = render(
|
render(
|
||||||
<EditorProviders
|
<EditorProviders
|
||||||
scope={{
|
scope={{
|
||||||
project: {
|
project: {
|
||||||
|
@ -197,7 +200,7 @@ describe('<ShareProjectModal/>', function () {
|
||||||
await screen.findByRole('button', { name: 'Resend' })
|
await screen.findByRole('button', { name: 'Resend' })
|
||||||
|
|
||||||
// render as non-admin (non-owner), link sharing on: actions should be missing and message should be present
|
// render as non-admin (non-owner), link sharing on: actions should be missing and message should be present
|
||||||
rerender(
|
render(
|
||||||
<EditorProviders
|
<EditorProviders
|
||||||
scope={{
|
scope={{
|
||||||
project: {
|
project: {
|
||||||
|
@ -222,7 +225,7 @@ describe('<ShareProjectModal/>', function () {
|
||||||
expect(screen.queryByRole('button', { name: 'Resend' })).to.be.null
|
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
|
// render as non-admin (non-owner), link sharing off: actions should be missing and message should be present
|
||||||
rerender(
|
render(
|
||||||
<EditorProviders
|
<EditorProviders
|
||||||
scope={{
|
scope={{
|
||||||
project: {
|
project: {
|
||||||
|
@ -619,10 +622,6 @@ describe('<ShareProjectModal/>', function () {
|
||||||
)
|
)
|
||||||
|
|
||||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||||
user: {
|
|
||||||
id: '123abd',
|
|
||||||
allowedFreeTrial: true,
|
|
||||||
},
|
|
||||||
scope: {
|
scope: {
|
||||||
project: {
|
project: {
|
||||||
...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 { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
|
||||||
import { DetachProvider } from '../../../frontend/js/shared/context/detach-context'
|
import { DetachProvider } from '../../../frontend/js/shared/context/detach-context'
|
||||||
import { LayoutProvider } from '../../../frontend/js/shared/context/layout-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
|
// these constants can be imported in tests instead of
|
||||||
// using magic strings
|
// using magic strings
|
||||||
|
@ -110,7 +111,9 @@ export function EditorProviders({
|
||||||
<EditorProvider settings={{}}>
|
<EditorProvider settings={{}}>
|
||||||
<DetachProvider>
|
<DetachProvider>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<CompileProvider>{children}</CompileProvider>
|
<LocalCompileProvider>
|
||||||
|
<DetachCompileProvider>{children}</DetachCompileProvider>
|
||||||
|
</LocalCompileProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</DetachProvider>
|
</DetachProvider>
|
||||||
</EditorProvider>
|
</EditorProvider>
|
||||||
|
|
|
@ -25,6 +25,10 @@ function getLastBroacastMessage() {
|
||||||
return getLastDetachCall('broadcast').args[1]
|
return getLastDetachCall('broadcast').args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllBroacastMessages() {
|
||||||
|
return getDetachCalls('broadcast')
|
||||||
|
}
|
||||||
|
|
||||||
// this fakes receiving a message by calling the handler add to `on`. A bit
|
// this fakes receiving a message by calling the handler add to `on`. A bit
|
||||||
// funky, but works for now
|
// funky, but works for now
|
||||||
function receiveMessage(message) {
|
function receiveMessage(message) {
|
||||||
|
@ -37,5 +41,6 @@ export default {
|
||||||
getDetachCalls,
|
getDetachCalls,
|
||||||
getLastDetachCall,
|
getLastDetachCall,
|
||||||
getLastBroacastMessage,
|
getLastBroacastMessage,
|
||||||
|
getAllBroacastMessages,
|
||||||
receiveMessage,
|
receiveMessage,
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ describe('useDetachLayout', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('detacher role', async function () {
|
it('detacher role', async function () {
|
||||||
|
sysendTestHelper.spy.broadcast.resetHistory()
|
||||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||||
|
|
||||||
// 1. create hook in detacher mode
|
// 1. create hook in detacher mode
|
||||||
|
@ -55,6 +56,8 @@ describe('useDetachLayout', function () {
|
||||||
expect(result.current.isLinked).to.be.false
|
expect(result.current.isLinked).to.be.false
|
||||||
expect(result.current.isLinking).to.be.false
|
expect(result.current.isLinking).to.be.false
|
||||||
expect(result.current.role).to.equal('detacher')
|
expect(result.current.role).to.equal('detacher')
|
||||||
|
const broadcastMessagesCount =
|
||||||
|
sysendTestHelper.getAllBroacastMessages().length
|
||||||
|
|
||||||
// 2. simulate connected detached tab
|
// 2. simulate connected detached tab
|
||||||
sysendTestHelper.spy.broadcast.resetHistory()
|
sysendTestHelper.spy.broadcast.resetHistory()
|
||||||
|
@ -70,6 +73,12 @@ describe('useDetachLayout', function () {
|
||||||
expect(result.current.isLinking).to.be.false
|
expect(result.current.isLinking).to.be.false
|
||||||
expect(result.current.role).to.equal('detacher')
|
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
|
// 3. simulate closed detached tab
|
||||||
sysendTestHelper.spy.broadcast.resetHistory()
|
sysendTestHelper.spy.broadcast.resetHistory()
|
||||||
sysendTestHelper.receiveMessage({
|
sysendTestHelper.receiveMessage({
|
||||||
|
@ -90,21 +99,7 @@ describe('useDetachLayout', function () {
|
||||||
expect(result.current.isLinking).to.be.false
|
expect(result.current.isLinking).to.be.false
|
||||||
expect(result.current.role).to.equal('detacher')
|
expect(result.current.role).to.equal('detacher')
|
||||||
|
|
||||||
// 5. simulate closed detacher tab
|
// 5. reattach
|
||||||
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
|
|
||||||
sysendTestHelper.spy.broadcast.resetHistory()
|
sysendTestHelper.spy.broadcast.resetHistory()
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.reattach()
|
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 () {
|
it('detached role', async function () {
|
||||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||||
|
|
||||||
|
@ -185,3 +200,9 @@ describe('useDetachLayout', function () {
|
||||||
sinon.assert.called(closeStub)
|
sinon.assert.called(closeStub)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const nextTick = () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ const entryPoints = {
|
||||||
serviceWorker: './frontend/js/serviceWorker.js',
|
serviceWorker: './frontend/js/serviceWorker.js',
|
||||||
main: './frontend/js/main.js',
|
main: './frontend/js/main.js',
|
||||||
ide: './frontend/js/ide.js',
|
ide: './frontend/js/ide.js',
|
||||||
|
'ide-detached': './frontend/js/ide-detached.js',
|
||||||
marketing: './frontend/js/marketing.js',
|
marketing: './frontend/js/marketing.js',
|
||||||
style: './frontend/stylesheets/style.less',
|
style: './frontend/stylesheets/style.less',
|
||||||
'ieee-style': './frontend/stylesheets/ieee-style.less',
|
'ieee-style': './frontend/stylesheets/ieee-style.less',
|
||||||
|
|
Loading…
Reference in a new issue