mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #23502 from overleaf/dp-pdf-typescript
Convert PDF Preview components to typescript GitOrigin-RevId: 34594d21143727fa42b8b595aa12125a4dd7ae5e
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import DetachCompileButton from './detach-compile-button'
|
||||
|
||||
function DetachCompileButtonWrapper() {
|
||||
const { detachRole, detachIsLinked } = useLayoutContext(
|
||||
layoutContextPropTypes
|
||||
)
|
||||
const { detachRole, detachIsLinked } = useLayoutContext()
|
||||
|
||||
if (detachRole !== 'detacher' || !detachIsLinked) {
|
||||
return null
|
||||
@@ -15,9 +12,4 @@ function DetachCompileButtonWrapper() {
|
||||
return <DetachCompileButton />
|
||||
}
|
||||
|
||||
const layoutContextPropTypes = {
|
||||
detachRole: PropTypes.string,
|
||||
detachIsLinked: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default memo(DetachCompileButtonWrapper)
|
||||
@@ -17,6 +17,10 @@ function PdfDownloadFilesButton() {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!fileList) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
@@ -42,7 +46,6 @@ function PdfDownloadFilesButton() {
|
||||
<DropdownToggle
|
||||
id="dropdown-files-logs-pane"
|
||||
variant="secondary"
|
||||
title={t('other_logs_and_files')}
|
||||
size="sm"
|
||||
disabled={compiling || !fileList}
|
||||
>
|
||||
@@ -1,22 +1,22 @@
|
||||
import { MenuItem as BS3MenuItem } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import {
|
||||
DropdownDivider,
|
||||
DropdownHeader,
|
||||
DropdownItem,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import { PdfFileData, PdfFileDataList } from '../util/types'
|
||||
|
||||
function PdfFileList({ fileList }) {
|
||||
function PdfFileList({ fileList }: { fileList: PdfFileDataList }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!fileList) {
|
||||
return null
|
||||
}
|
||||
|
||||
function basename(file) {
|
||||
function basename(file: PdfFileData) {
|
||||
return file.path.split('/').pop()
|
||||
}
|
||||
|
||||
@@ -94,40 +94,22 @@ function PdfFileList({ fileList }) {
|
||||
</li>
|
||||
))}
|
||||
|
||||
{fileList.archive?.fileCount > 0 && (
|
||||
<li role="menuitem">
|
||||
<DropdownItem
|
||||
role="link"
|
||||
download={basename(fileList.archive)}
|
||||
href={fileList.archive.url}
|
||||
>
|
||||
{t('download_all')} ({fileList.archive.fileCount})
|
||||
</DropdownItem>
|
||||
</li>
|
||||
)}
|
||||
{fileList.archive?.fileCount !== undefined &&
|
||||
fileList.archive?.fileCount > 0 && (
|
||||
<li role="menuitem">
|
||||
<DropdownItem
|
||||
role="link"
|
||||
download={basename(fileList.archive)}
|
||||
href={fileList.archive.url}
|
||||
>
|
||||
{t('download_all')} ({fileList.archive.fileCount})
|
||||
</DropdownItem>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const FilesArray = PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
path: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
})
|
||||
)
|
||||
|
||||
PdfFileList.propTypes = {
|
||||
fileList: PropTypes.shape({
|
||||
top: FilesArray,
|
||||
other: FilesArray,
|
||||
archive: PropTypes.shape({
|
||||
path: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
fileCount: PropTypes.number.isRequired,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export default memo(PdfFileList)
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PdfLogEntryRawContent from './pdf-log-entry-raw-content'
|
||||
import PropTypes from 'prop-types'
|
||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
import { LogEntry } from '../util/types'
|
||||
import { ElementType } from 'react'
|
||||
|
||||
const pdfLogEntryComponents = importOverleafModules('pdfLogEntryComponents')
|
||||
const pdfLogEntryComponents = importOverleafModules(
|
||||
'pdfLogEntryComponents'
|
||||
) as {
|
||||
import: { default: ElementType }
|
||||
path: string
|
||||
}[]
|
||||
|
||||
export default function PdfLogEntryContent({
|
||||
rawContent,
|
||||
@@ -11,6 +17,12 @@ export default function PdfLogEntryContent({
|
||||
extraInfoURL,
|
||||
index,
|
||||
logEntry,
|
||||
}: {
|
||||
rawContent?: string
|
||||
formattedContent?: React.ReactNode
|
||||
extraInfoURL?: string | null
|
||||
index?: number
|
||||
logEntry?: LogEntry
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -41,11 +53,3 @@ export default function PdfLogEntryContent({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
PdfLogEntryContent.propTypes = {
|
||||
rawContent: PropTypes.string,
|
||||
formattedContent: PropTypes.node,
|
||||
extraInfoURL: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
logEntry: PropTypes.any,
|
||||
}
|
||||
@@ -4,11 +4,13 @@ import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'classnames'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default function PdfLogEntryRawContent({
|
||||
rawContent,
|
||||
collapsedSize = 0,
|
||||
}: {
|
||||
rawContent: string
|
||||
collapsedSize?: number
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [needsExpander, setNeedsExpander] = useState(true)
|
||||
@@ -68,8 +70,3 @@ export default function PdfLogEntryRawContent({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
PdfLogEntryRawContent.propTypes = {
|
||||
rawContent: PropTypes.string.isRequired,
|
||||
collapsedSize: PropTypes.number,
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { memo, MouseEventHandler, useCallback } from 'react'
|
||||
import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-header'
|
||||
import PdfLogEntryContent from './pdf-log-entry-content'
|
||||
import HumanReadableLogsHints from '../../../ide/human-readable-logs/HumanReadableLogsHints'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { ErrorLevel, LogEntry, SourceLocation } from '../util/types'
|
||||
|
||||
function PdfLogEntry({
|
||||
ruleId,
|
||||
@@ -19,7 +19,7 @@ function PdfLogEntry({
|
||||
sourceLocation,
|
||||
showSourceLocationLink = true,
|
||||
showCloseButton = false,
|
||||
entryAriaLabel = null,
|
||||
entryAriaLabel = undefined,
|
||||
customClass,
|
||||
contentDetails,
|
||||
onSourceLocationClick,
|
||||
@@ -27,6 +27,26 @@ function PdfLogEntry({
|
||||
index,
|
||||
logEntry,
|
||||
id,
|
||||
}: {
|
||||
headerTitle: string | React.ReactNode
|
||||
level: ErrorLevel
|
||||
ruleId?: string
|
||||
headerIcon?: React.ReactElement
|
||||
rawContent?: string
|
||||
logType?: string
|
||||
formattedContent?: React.ReactNode
|
||||
extraInfoURL?: string | null
|
||||
sourceLocation?: SourceLocation
|
||||
showSourceLocationLink?: boolean
|
||||
showCloseButton?: boolean
|
||||
entryAriaLabel?: string
|
||||
customClass?: string
|
||||
contentDetails?: string[]
|
||||
onSourceLocationClick?: (sourceLocation: SourceLocation) => void
|
||||
onClose?: () => void
|
||||
index?: number
|
||||
logEntry?: LogEntry
|
||||
id?: string
|
||||
}) {
|
||||
const showAiErrorAssistant = getMeta('ol-showAiErrorAssistant')
|
||||
|
||||
@@ -36,17 +56,22 @@ function PdfLogEntry({
|
||||
extraInfoURL = hint.extraInfoURL
|
||||
}
|
||||
|
||||
const handleLogEntryLinkClick = useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
onSourceLocationClick(sourceLocation)
|
||||
const handleLogEntryLinkClick: MouseEventHandler<HTMLButtonElement> =
|
||||
useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
|
||||
const parts = sourceLocation?.file?.split('.')
|
||||
const extension = parts?.length > 1 ? parts.pop() : ''
|
||||
sendMB('log-entry-link-click', { level, ruleId, extension })
|
||||
},
|
||||
[level, onSourceLocationClick, ruleId, sourceLocation]
|
||||
)
|
||||
if (onSourceLocationClick && sourceLocation) {
|
||||
onSourceLocationClick(sourceLocation)
|
||||
|
||||
const parts = sourceLocation?.file?.split('.')
|
||||
const extension =
|
||||
parts?.length && parts?.length > 1 ? parts.pop() : ''
|
||||
sendMB('log-entry-link-click', { level, ruleId, extension })
|
||||
}
|
||||
},
|
||||
[level, onSourceLocationClick, ruleId, sourceLocation]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -80,33 +105,4 @@ function PdfLogEntry({
|
||||
)
|
||||
}
|
||||
|
||||
PdfLogEntry.propTypes = {
|
||||
ruleId: PropTypes.string,
|
||||
sourceLocation: PreviewLogEntryHeader.propTypes.sourceLocation,
|
||||
headerTitle: PreviewLogEntryHeader.propTypes.headerTitle,
|
||||
headerIcon: PropTypes.element,
|
||||
rawContent: PropTypes.string,
|
||||
logType: PropTypes.string,
|
||||
formattedContent: PropTypes.node,
|
||||
extraInfoURL: PropTypes.string,
|
||||
level: PropTypes.oneOf([
|
||||
'error',
|
||||
'warning',
|
||||
'info',
|
||||
'typesetting',
|
||||
'raw',
|
||||
'success',
|
||||
]).isRequired,
|
||||
customClass: PropTypes.string,
|
||||
showSourceLocationLink: PropTypes.bool,
|
||||
showCloseButton: PropTypes.bool,
|
||||
entryAriaLabel: PropTypes.string,
|
||||
contentDetails: PropTypes.arrayOf(PropTypes.string),
|
||||
onSourceLocationClick: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
index: PropTypes.number,
|
||||
logEntry: PropTypes.any,
|
||||
id: PropTypes.string,
|
||||
}
|
||||
|
||||
export default memo(PdfLogEntry)
|
||||
@@ -1,16 +1,27 @@
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ElementType, memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewLogsPaneMaxEntries from '../../preview/components/preview-logs-pane-max-entries'
|
||||
import PdfLogEntry from './pdf-log-entry'
|
||||
import { useDetachCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
import { LogEntry } from '../util/types'
|
||||
|
||||
const LOG_PREVIEW_LIMIT = 100
|
||||
|
||||
const pdfLogEntriesComponents = importOverleafModules('pdfLogEntriesComponents')
|
||||
const pdfLogEntriesComponents = importOverleafModules(
|
||||
'pdfLogEntriesComponents'
|
||||
) as {
|
||||
import: { default: ElementType }
|
||||
path: string
|
||||
}[]
|
||||
|
||||
function PdfLogsEntries({ entries, hasErrors }) {
|
||||
function PdfLogsEntries({
|
||||
entries,
|
||||
hasErrors,
|
||||
}: {
|
||||
entries: LogEntry[]
|
||||
hasErrors?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { syncToEntry } = useDetachCompileContext()
|
||||
const logEntries = entries.slice(0, LOG_PREVIEW_LIMIT)
|
||||
@@ -57,9 +68,5 @@ function PdfLogsEntries({ entries, hasErrors }) {
|
||||
</>
|
||||
)
|
||||
}
|
||||
PdfLogsEntries.propTypes = {
|
||||
entries: PropTypes.arrayOf(PropTypes.object),
|
||||
hasErrors: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default memo(PdfLogsEntries)
|
||||
@@ -14,11 +14,10 @@ import PdfCodeCheckFailedNotice from './pdf-code-check-failed-notice'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import PdfLogEntry from './pdf-log-entry'
|
||||
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
import PropTypes from 'prop-types'
|
||||
import TimeoutUpgradePaywallPrompt from './timeout-upgrade-paywall-prompt'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function PdfLogsViewer({ alwaysVisible = false }) {
|
||||
function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
|
||||
const {
|
||||
codeCheckFailed,
|
||||
error,
|
||||
@@ -91,10 +90,6 @@ function PdfLogsViewer({ alwaysVisible = false }) {
|
||||
)
|
||||
}
|
||||
|
||||
PdfLogsViewer.propTypes = {
|
||||
alwaysVisible: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default withErrorBoundary(memo(PdfLogsViewer), () => (
|
||||
<PdfPreviewErrorBoundaryFallback type="logs" />
|
||||
))
|
||||
@@ -1,8 +1,11 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback'
|
||||
|
||||
function PdfPreviewErrorBoundaryFallback({ type }) {
|
||||
function PdfPreviewErrorBoundaryFallback({
|
||||
type,
|
||||
}: {
|
||||
type: 'preview' | 'pdf' | 'logs'
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const showInfoLink = (
|
||||
@@ -47,8 +50,4 @@ function PdfPreviewErrorBoundaryFallback({ type }) {
|
||||
}
|
||||
}
|
||||
|
||||
PdfPreviewErrorBoundaryFallback.propTypes = {
|
||||
type: PropTypes.oneOf(['preview', 'pdf', 'logs']).isRequired,
|
||||
}
|
||||
|
||||
export default PdfPreviewErrorBoundaryFallback
|
||||
@@ -1,4 +1,3 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { memo, useCallback } from 'react'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
@@ -7,7 +6,7 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co
|
||||
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
|
||||
import getMeta from '../../../utils/meta'
|
||||
|
||||
function PdfPreviewError({ error }) {
|
||||
function PdfPreviewError({ error }: { error: string }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { startCompile } = useCompileContext()
|
||||
@@ -216,13 +215,17 @@ function PdfPreviewError({ error }) {
|
||||
}
|
||||
}
|
||||
|
||||
PdfPreviewError.propTypes = {
|
||||
error: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default memo(PdfPreviewError)
|
||||
|
||||
function ErrorLogEntry({ title, headerIcon, children }) {
|
||||
function ErrorLogEntry({
|
||||
title,
|
||||
headerIcon,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
headerIcon?: React.ReactElement
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
@@ -235,11 +238,6 @@ function ErrorLogEntry({ title, headerIcon, children }) {
|
||||
/>
|
||||
)
|
||||
}
|
||||
ErrorLogEntry.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
headerIcon: PropTypes.element,
|
||||
children: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
function TimedOutLogEntry() {
|
||||
const { t } = useTranslation()
|
||||
@@ -307,4 +305,3 @@ function TimedOutLogEntry() {
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
}
|
||||
TimedOutLogEntry.propTypes = {}
|
||||
@@ -18,18 +18,18 @@ const ORPHAN_UI_TIMEOUT_MS = 5000
|
||||
function PdfPreviewHybridToolbar() {
|
||||
const { detachRole, detachIsLinked } = useLayoutContext()
|
||||
|
||||
const uiTimeoutRef = useRef()
|
||||
const uiTimeoutRef = useRef<number>()
|
||||
const [orphanPdfTabAfterDelay, setOrphanPdfTabAfterDelay] = useState(false)
|
||||
|
||||
const orphanPdfTab = !detachIsLinked && detachRole === 'detached'
|
||||
|
||||
useEffect(() => {
|
||||
if (uiTimeoutRef.current) {
|
||||
clearTimeout(uiTimeoutRef.current)
|
||||
window.clearTimeout(uiTimeoutRef.current)
|
||||
}
|
||||
|
||||
if (orphanPdfTab) {
|
||||
uiTimeoutRef.current = setTimeout(() => {
|
||||
uiTimeoutRef.current = window.setTimeout(() => {
|
||||
setOrphanPdfTabAfterDelay(true)
|
||||
}, ORPHAN_UI_TIMEOUT_MS)
|
||||
} else {
|
||||
@@ -1,6 +1,5 @@
|
||||
import classNames from 'classnames'
|
||||
import { memo, useCallback, useEffect, useState, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { getJSON } from '../../../infrastructure/fetch-json'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
@@ -25,12 +24,20 @@ import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { PdfScrollPosition } from '@/shared/hooks/use-pdf-scroll-position'
|
||||
import { CursorPosition } from '@/features/ide-react/types/cursor-position'
|
||||
|
||||
function GoToCodeButton({
|
||||
position,
|
||||
syncToCode,
|
||||
syncToCodeInFlight,
|
||||
isDetachLayout,
|
||||
}: {
|
||||
position: PdfScrollPosition
|
||||
syncToCode: (position: PdfScrollPosition, visualOffset?: number) => void
|
||||
syncToCodeInFlight: boolean
|
||||
isDetachLayout?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
|
||||
@@ -89,7 +96,7 @@ function GoToCodeButton({
|
||||
className={buttonClasses}
|
||||
aria-label={t('go_to_pdf_location_in_code')}
|
||||
bs3Props={{
|
||||
bsSize: 'xs',
|
||||
bsSize: 'xsmall',
|
||||
}}
|
||||
>
|
||||
{buttonIcon}
|
||||
@@ -105,6 +112,12 @@ function GoToPdfButton({
|
||||
syncToPdfInFlight,
|
||||
isDetachLayout,
|
||||
hasSingleSelectedDoc,
|
||||
}: {
|
||||
cursorPosition: CursorPosition | null
|
||||
syncToPdf: (cursorPosition: CursorPosition | null) => void
|
||||
syncToPdfInFlight: boolean
|
||||
hasSingleSelectedDoc: boolean
|
||||
isDetachLayout?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
|
||||
@@ -159,7 +172,7 @@ function GoToPdfButton({
|
||||
className={buttonClasses}
|
||||
aria-label={t('go_to_code_location_in_pdf')}
|
||||
bs3Props={{
|
||||
bsSize: 'xs',
|
||||
bsSize: 'xsmall',
|
||||
}}
|
||||
>
|
||||
{buttonIcon}
|
||||
@@ -187,22 +200,24 @@ function PdfSynctexControls() {
|
||||
const { findEntityByPath, dirname, pathInFolder } = useFileTreePathContext()
|
||||
const { getCurrentDocumentId, openDocWithId } = useEditorManagerContext()
|
||||
|
||||
const [cursorPosition, setCursorPosition] = useState(() => {
|
||||
const position = localStorage.getItem(
|
||||
`doc.position.${getCurrentDocumentId()}`
|
||||
)
|
||||
return position ? position.cursorPosition : null
|
||||
})
|
||||
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(
|
||||
() => {
|
||||
const position = localStorage.getItem(
|
||||
`doc.position.${getCurrentDocumentId()}`
|
||||
)
|
||||
return position ? position.cursorPosition : null
|
||||
}
|
||||
)
|
||||
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const { signal } = useAbortController()
|
||||
|
||||
useEffect(() => {
|
||||
const listener = event => setCursorPosition(event.detail)
|
||||
window.addEventListener('cursor:editor:update', listener)
|
||||
return () => window.removeEventListener('cursor:editor:update', listener)
|
||||
}, [])
|
||||
const editorUpdateListener = useCallback(
|
||||
event => setCursorPosition(event.detail),
|
||||
[]
|
||||
)
|
||||
useEventListener('cursor:editor:update', editorUpdateListener)
|
||||
|
||||
const [syncToPdfInFlight, setSyncToPdfInFlight] = useState(false)
|
||||
const [syncToCodeInFlight, setSyncToCodeInFlight] = useDetachState(
|
||||
@@ -216,8 +231,17 @@ function PdfSynctexControls() {
|
||||
|
||||
const getCurrentFilePath = useCallback(() => {
|
||||
const docId = getCurrentDocumentId()
|
||||
|
||||
if (!docId || !rootDocId) {
|
||||
return null
|
||||
}
|
||||
|
||||
let path = pathInFolder(docId)
|
||||
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If the root file is folder/main.tex, then synctex sees the path as folder/./main.tex
|
||||
const rootDocDirname = dirname(rootDocId)
|
||||
|
||||
@@ -286,8 +310,10 @@ function PdfSynctexControls() {
|
||||
|
||||
const syncToPdf = useCallback(
|
||||
cursorPosition => {
|
||||
const file = getCurrentFilePath()
|
||||
|
||||
const params = new URLSearchParams({
|
||||
file: getCurrentFilePath(),
|
||||
file: file ?? '',
|
||||
line: cursorPosition.row + 1,
|
||||
column: cursorPosition.column,
|
||||
}).toString()
|
||||
@@ -377,13 +403,11 @@ function PdfSynctexControls() {
|
||||
'detacher'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const listener = event => syncToCode(event.detail)
|
||||
window.addEventListener('synctex:sync-to-position', listener)
|
||||
return () => {
|
||||
window.removeEventListener('synctex:sync-to-position', listener)
|
||||
}
|
||||
}, [syncToCode])
|
||||
const syncToPositionListener = useCallback(
|
||||
event => syncToCode(event.detail),
|
||||
[syncToCode]
|
||||
)
|
||||
useEventListener('synctex:sync-to-position', syncToPositionListener)
|
||||
|
||||
const [hasSingleSelectedDoc, setHasSingleSelectedDoc] = useDetachState(
|
||||
'has-single-selected-doc',
|
||||
@@ -458,18 +482,3 @@ function PdfSynctexControls() {
|
||||
}
|
||||
|
||||
export default memo(PdfSynctexControls)
|
||||
|
||||
GoToCodeButton.propTypes = {
|
||||
isDetachLayout: PropTypes.bool,
|
||||
position: PropTypes.object.isRequired,
|
||||
syncToCode: PropTypes.func.isRequired,
|
||||
syncToCodeInFlight: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
GoToPdfButton.propTypes = {
|
||||
cursorPosition: PropTypes.object,
|
||||
isDetachLayout: PropTypes.bool,
|
||||
syncToPdf: PropTypes.func.isRequired,
|
||||
syncToPdfInFlight: PropTypes.bool.isRequired,
|
||||
hasSingleSelectedDoc: PropTypes.bool.isRequired,
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PropTypes from 'prop-types'
|
||||
import PdfLogEntry from './pdf-log-entry'
|
||||
|
||||
PdfValidationIssue.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
issue: PropTypes.any,
|
||||
}
|
||||
|
||||
function PdfValidationIssue({ issue, name }) {
|
||||
function PdfValidationIssue({ issue, name }: { issue: any; name: string }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
switch (name) {
|
||||
@@ -20,12 +14,14 @@ function PdfValidationIssue({ issue, name }) {
|
||||
<>
|
||||
<div>{t('project_too_large_please_reduce')}</div>
|
||||
<ul className="list-no-margin-bottom">
|
||||
{issue.resources.map(resource => (
|
||||
<li key={resource.path}>
|
||||
{resource.path} — {resource.kbSize}
|
||||
kb
|
||||
</li>
|
||||
))}
|
||||
{issue.resources.map(
|
||||
(resource: { path: string; kbSize: number }) => (
|
||||
<li key={resource.path}>
|
||||
{resource.path} — {resource.kbSize}
|
||||
kb
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
@@ -42,7 +38,7 @@ function PdfValidationIssue({ issue, name }) {
|
||||
<>
|
||||
<div>{t('following_paths_conflict')}</div>
|
||||
<ul className="list-no-margin-bottom">
|
||||
{issue.map(detail => (
|
||||
{issue.map((detail: { path: string }) => (
|
||||
<li key={detail.path}>/{detail.path}</li>
|
||||
))}
|
||||
</ul>
|
||||
41
services/web/frontend/js/features/pdf-preview/util/types.ts
Normal file
41
services/web/frontend/js/features/pdf-preview/util/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
|
||||
export type LogEntry = {
|
||||
raw: string
|
||||
level: ErrorLevel
|
||||
key: string
|
||||
file?: string
|
||||
column?: number
|
||||
line?: number
|
||||
ruleId?: string
|
||||
message?: string
|
||||
content?: string
|
||||
type?: string
|
||||
messageComponent?: React.ReactNode
|
||||
contentDetails?: string[]
|
||||
}
|
||||
|
||||
export type ErrorLevel =
|
||||
| 'error'
|
||||
| 'warning'
|
||||
| 'info'
|
||||
| 'typesetting'
|
||||
| 'raw'
|
||||
| 'success'
|
||||
|
||||
export type SourceLocation = {
|
||||
file?: string
|
||||
// `line should be either a number or null (i.e. not required), but currently sometimes we get
|
||||
// an empty string (from BibTeX errors).
|
||||
line?: number | string | null
|
||||
column?: number
|
||||
}
|
||||
|
||||
export type PdfFileData = { path: string; url: string }
|
||||
type PdfFileArchiveData = { path: string; url: string; fileCount: number }
|
||||
|
||||
export type PdfFileDataList = {
|
||||
top: PdfFileData[]
|
||||
other: PdfFileData[]
|
||||
archive?: PdfFileArchiveData
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import { useState, useRef } from 'react'
|
||||
import { useState, useRef, MouseEventHandler } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useResizeObserver from '../hooks/use-resize-observer'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
@@ -8,6 +7,7 @@ import Icon from '../../../shared/components/icon'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { ErrorLevel, SourceLocation } from '@/features/pdf-preview/util/types'
|
||||
|
||||
function PreviewLogEntryHeader({
|
||||
sourceLocation,
|
||||
@@ -19,9 +19,19 @@ function PreviewLogEntryHeader({
|
||||
showCloseButton = false,
|
||||
onSourceLocationClick,
|
||||
onClose,
|
||||
}: {
|
||||
headerTitle: string | React.ReactNode
|
||||
level: ErrorLevel
|
||||
headerIcon?: React.ReactElement
|
||||
logType?: string
|
||||
sourceLocation?: SourceLocation
|
||||
showSourceLocationLink?: boolean
|
||||
showCloseButton?: boolean
|
||||
onSourceLocationClick?: MouseEventHandler<HTMLButtonElement>
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const logLocationSpanRef = useRef()
|
||||
const logLocationSpanRef = useRef<HTMLSpanElement>(null)
|
||||
const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
|
||||
|
||||
useResizeObserver(
|
||||
@@ -52,7 +62,7 @@ function PreviewLogEntryHeader({
|
||||
location: file + (line ? `, ${line}` : ''),
|
||||
})
|
||||
|
||||
function checkLocationSpanOverflow(observedElement) {
|
||||
function checkLocationSpanOverflow(observedElement: ResizeObserverEntry) {
|
||||
const spanEl = observedElement.target
|
||||
const isOverflowing = spanEl.scrollWidth > spanEl.clientWidth
|
||||
setLocationSpanOverflown(isOverflowing)
|
||||
@@ -104,7 +114,7 @@ function PreviewLogEntryHeader({
|
||||
<div className="log-entry-header-icon-container">{headerIcon}</div>
|
||||
) : null}
|
||||
<h3 className="log-entry-header-title">{headerTitleText}</h3>
|
||||
{locationSpanOverflown && locationLinkText ? (
|
||||
{locationSpanOverflown && locationLinkText && locationLink ? (
|
||||
<OLTooltip
|
||||
id={locationLinkText}
|
||||
description={locationLinkText}
|
||||
@@ -130,23 +140,4 @@ function PreviewLogEntryHeader({
|
||||
)
|
||||
}
|
||||
|
||||
PreviewLogEntryHeader.propTypes = {
|
||||
sourceLocation: PropTypes.shape({
|
||||
file: PropTypes.string,
|
||||
// `line should be either a number or null (i.e. not required), but currently sometimes we get
|
||||
// an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
|
||||
// to PropTypes.number (not required) once we fix that.
|
||||
line: PropTypes.any,
|
||||
column: PropTypes.any,
|
||||
}),
|
||||
level: PropTypes.string.isRequired,
|
||||
headerTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
headerIcon: PropTypes.element,
|
||||
logType: PropTypes.string,
|
||||
showSourceLocationLink: PropTypes.bool,
|
||||
showCloseButton: PropTypes.bool,
|
||||
onSourceLocationClick: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
}
|
||||
|
||||
export default PreviewLogEntryHeader
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useLayoutEffect, useRef, useCallback } from 'react'
|
||||
import { useLayoutEffect, useRef, useCallback, RefObject } from 'react'
|
||||
|
||||
function useResizeObserver(observedElement, observedData, callback) {
|
||||
const resizeObserver = useRef()
|
||||
function useResizeObserver(
|
||||
observedElement: RefObject<HTMLElement>,
|
||||
observedData: any,
|
||||
callback: (observedElement: ResizeObserverEntry) => void
|
||||
) {
|
||||
const resizeObserver = useRef<ResizeObserver>()
|
||||
|
||||
const observe = useCallback(() => {
|
||||
resizeObserver.current = new ResizeObserver(function (elementsObserved) {
|
||||
@@ -9,15 +13,17 @@ function useResizeObserver(observedElement, observedData, callback) {
|
||||
})
|
||||
}, [callback])
|
||||
|
||||
function unobserve(observedCurrent) {
|
||||
resizeObserver.current.unobserve(observedCurrent)
|
||||
function unobserve(observedCurrent: HTMLElement) {
|
||||
if (resizeObserver.current) {
|
||||
resizeObserver.current.unobserve(observedCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if ('ResizeObserver' in window) {
|
||||
const observedCurrent = observedElement && observedElement.current
|
||||
if (observedCurrent) {
|
||||
observe(observedElement.current)
|
||||
observe()
|
||||
}
|
||||
|
||||
if (resizeObserver.current && observedCurrent) {
|
||||
@@ -34,4 +34,5 @@ export type ButtonProps = {
|
||||
| 'premium'
|
||||
| 'premium-secondary'
|
||||
| 'link'
|
||||
| 'info'
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
packageSuggestionsForCommands,
|
||||
packageSuggestionsForEnvironments,
|
||||
} from './HumanReadableLogsPackageSuggestions'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function WikiLink({ url, children }) {
|
||||
function WikiLink({
|
||||
url,
|
||||
children,
|
||||
}: {
|
||||
url: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
if (getMeta('ol-wikiEnabled')) {
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener">
|
||||
@@ -17,12 +22,12 @@ function WikiLink({ url, children }) {
|
||||
}
|
||||
}
|
||||
|
||||
WikiLink.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
type LogHint = {
|
||||
extraInfoURL?: string | null
|
||||
formattedContent: (details?: string[]) => React.ReactNode
|
||||
}
|
||||
|
||||
const hints = {
|
||||
const hints: { [ruleId: string]: LogHint } = {
|
||||
hint_misplaced_alignment_tab_character: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Misplaced_alignment_tab_character_%26',
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
PdfScrollPosition,
|
||||
usePdfScrollPosition,
|
||||
} from '@/shared/hooks/use-pdf-scroll-position'
|
||||
import { PdfFileDataList } from '@/features/pdf-preview/util/types'
|
||||
|
||||
type PdfFile = Record<string, any>
|
||||
|
||||
@@ -54,7 +55,7 @@ export type CompileContext = {
|
||||
deliveryLatencies: Record<string, any>
|
||||
draft: boolean
|
||||
error?: string
|
||||
fileList?: Record<string, any>
|
||||
fileList?: PdfFileDataList
|
||||
hasChanges: boolean
|
||||
hasShortCompileTimeout: boolean
|
||||
highlights?: Record<string, any>[]
|
||||
@@ -208,7 +209,7 @@ export const LocalCompileProvider: FC = ({ children }) => {
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
// the list of files that can be downloaded
|
||||
const [fileList, setFileList] = useState<Record<string, any[]>>()
|
||||
const [fileList, setFileList] = useState<PdfFileDataList>()
|
||||
|
||||
// the raw contents of the log file
|
||||
const [rawLog, setRawLog] = useState<string>()
|
||||
|
||||
@@ -126,6 +126,7 @@ export interface Meta {
|
||||
'ol-inviterName': string
|
||||
'ol-isExternalAuthenticationSystemUsed': boolean
|
||||
'ol-isManagedAccount': boolean
|
||||
'ol-isPaywallChangeCompileTimeoutEnabled': boolean
|
||||
'ol-isProfessional': boolean
|
||||
'ol-isRegisteredViaGoogle': boolean
|
||||
'ol-isRestrictedTokenMember': boolean
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useMeta } from './hooks/use-meta'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { useScope } from './hooks/use-scope'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { LogEntry } from '@/features/pdf-preview/util/types'
|
||||
|
||||
const fakeSourceLocation = {
|
||||
file: 'file.tex',
|
||||
@@ -14,7 +15,7 @@ const fakeSourceLocation = {
|
||||
column: 5,
|
||||
}
|
||||
|
||||
const fakeLogEntry = {
|
||||
const fakeLogEntry: LogEntry = {
|
||||
key: 'fake',
|
||||
ruleId: 'hint_misplaced_alignment_tab_character',
|
||||
message: 'Fake message',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/features/ide-react/context/editor-manager-context'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { OpenDocuments } from '@/features/ide-react/editor/open-documents'
|
||||
import { LogEntry } from '@/features/pdf-preview/util/types'
|
||||
|
||||
describe('<PdfLogsEntries/>', function () {
|
||||
const fakeFindEntityResult: FindResult = {
|
||||
@@ -48,7 +49,7 @@ describe('<PdfLogsEntries/>', function () {
|
||||
)
|
||||
}
|
||||
|
||||
const logEntries = [
|
||||
const logEntries: LogEntry[] = [
|
||||
{
|
||||
file: 'main.tex',
|
||||
line: 9,
|
||||
|
||||
Reference in New Issue
Block a user