Просмотр исходного кода

Merge pull request #23502 from overleaf/dp-pdf-typescript

Convert PDF Preview components to typescript

GitOrigin-RevId: 34594d21143727fa42b8b595aa12125a4dd7ae5e
David 1 год назад
Родитель
Сommit
1d648f9755
30 измененных файлов с 253 добавлено и 228 удалено
  1. 1 9
      services/web/frontend/js/features/pdf-preview/components/detach-compile-button-wrapper.tsx
  2. 0 0
      services/web/frontend/js/features/pdf-preview/components/detach-synctex-control.tsx
  3. 0 0
      services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.tsx
  4. 0 0
      services/web/frontend/js/features/pdf-preview/components/pdf-code-check-failed-notice.tsx
  5. 4 1
      services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.tsx
  6. 15 33
      services/web/frontend/js/features/pdf-preview/components/pdf-file-list.tsx
  7. 0 0
      services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-code-check-button.tsx
  8. 14 10
      services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-content.tsx
  9. 3 6
      services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx
  10. 38 42
      services/web/frontend/js/features/pdf-preview/components/pdf-log-entry.tsx
  11. 15 8
      services/web/frontend/js/features/pdf-preview/components/pdf-logs-entries.tsx
  12. 1 6
      services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.tsx
  13. 0 0
      services/web/frontend/js/features/pdf-preview/components/pdf-orphan-refresh-button.tsx
  14. 0 0
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.tsx
  15. 5 6
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-error-boundary-fallback.tsx
  16. 10 13
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.tsx
  17. 3 3
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.tsx
  18. 0 0
      services/web/frontend/js/features/pdf-preview/components/pdf-preview.tsx
  19. 46 37
      services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx
  20. 10 14
      services/web/frontend/js/features/pdf-preview/components/pdf-validation-issue.tsx
  21. 0 0
      services/web/frontend/js/features/pdf-preview/components/stop-on-first-error-prompt.tsx
  22. 41 0
      services/web/frontend/js/features/pdf-preview/util/types.ts
  23. 15 24
      services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx
  24. 12 6
      services/web/frontend/js/features/preview/hooks/use-resize-observer.ts
  25. 1 0
      services/web/frontend/js/features/ui/components/types/button-props.ts
  26. 11 6
      services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsHints.tsx
  27. 3 2
      services/web/frontend/js/shared/context/local-compile-context.tsx
  28. 1 0
      services/web/frontend/js/utils/meta.ts
  29. 2 1
      services/web/frontend/stories/pdf-log-entry.stories.tsx
  30. 2 1
      services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx

+ 1 - 9
services/web/frontend/js/features/pdf-preview/components/detach-compile-button-wrapper.jsx → services/web/frontend/js/features/pdf-preview/components/detach-compile-button-wrapper.tsx

@@ -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)

+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/detach-synctex-control.jsx → services/web/frontend/js/features/pdf-preview/components/detach-synctex-control.tsx


+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.tsx


+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-code-check-failed-notice.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-code-check-failed-notice.tsx


+ 4 - 1
services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.tsx

@@ -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}
           >

+ 15 - 33
services/web/frontend/js/features/pdf-preview/components/pdf-file-list.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-file-list.tsx

@@ -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)

+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-code-check-button.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-code-check-button.tsx


+ 14 - 10
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-content.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-content.tsx

@@ -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,
-}

+ 3 - 6
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx

@@ -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,
-}

+ 38 - 42
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-log-entry.tsx

@@ -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)

+ 15 - 8
services/web/frontend/js/features/pdf-preview/components/pdf-logs-entries.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-logs-entries.tsx

@@ -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)

+ 1 - 6
services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.tsx

@@ -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" />
 ))

+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-orphan-refresh-button.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-orphan-refresh-button.tsx


+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.tsx


+ 5 - 6
services/web/frontend/js/features/pdf-preview/components/pdf-preview-error-boundary-fallback.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-preview-error-boundary-fallback.tsx

@@ -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

+ 10 - 13
services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.tsx

@@ -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 = {}

+ 3 - 3
services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.tsx

@@ -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 {

+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-preview.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-preview.tsx


+ 46 - 37
services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx

@@ -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,
-}

+ 10 - 14
services/web/frontend/js/features/pdf-preview/components/pdf-validation-issue.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-validation-issue.tsx

@@ -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} &mdash; {resource.kbSize}
-                    kb
-                  </li>
-                ))}
+                {issue.resources.map(
+                  (resource: { path: string; kbSize: number }) => (
+                    <li key={resource.path}>
+                      {resource.path} &mdash; {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>

+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/stop-on-first-error-prompt.jsx → services/web/frontend/js/features/pdf-preview/components/stop-on-first-error-prompt.tsx


+ 41 - 0
services/web/frontend/js/features/pdf-preview/util/types.ts

@@ -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
+}

+ 15 - 24
services/web/frontend/js/features/preview/components/preview-log-entry-header.jsx → services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx

@@ -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

+ 12 - 6
services/web/frontend/js/features/preview/hooks/use-resize-observer.js → services/web/frontend/js/features/preview/hooks/use-resize-observer.ts

@@ -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) {

+ 1 - 0
services/web/frontend/js/features/ui/components/types/button-props.ts

@@ -34,4 +34,5 @@ export type ButtonProps = {
     | 'premium'
     | 'premium-secondary'
     | 'link'
+    | 'info'
 }

+ 11 - 6
services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsHints.jsx → services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsHints.tsx

@@ -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',

+ 3 - 2
services/web/frontend/js/shared/context/local-compile-context.tsx

@@ -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>()

+ 1 - 0
services/web/frontend/js/utils/meta.ts

@@ -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

+ 2 - 1
services/web/frontend/stories/pdf-log-entry.stories.tsx

@@ -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',

+ 2 - 1
services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx

@@ -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,