Files
overleaf-cep/services/web/frontend/js/features/pdf-preview/components/log-entry-header.tsx
T
Davinder Singh 84cdbfc12f Tearing down of Error Logs and pdf preview in editor (#31267)
GitOrigin-RevId: 05b2c980d874de801ffcdadb57dd9da9d983225c
2026-02-11 09:06:38 +00:00

191 lines
6.4 KiB
TypeScript

import classNames from 'classnames'
import {
useState,
useRef,
MouseEventHandler,
ElementType,
useCallback,
KeyboardEventHandler,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import {
ErrorLevel,
SourceLocation,
LogEntry as LogEntryData,
} from '@/features/pdf-preview/util/types'
import useResizeObserver from '@/features/preview/hooks/use-resize-observer'
import OLIconButton from '@/shared/components/ol/ol-icon-button'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import MaterialIcon from '@/shared/components/material-icon'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
const actionComponents = importOverleafModules(
'pdfLogEntryHeaderActionComponents'
) as {
import: { default: ElementType }
path: string
}[]
function LogEntryHeader({
sourceLocation,
level,
headerTitle,
logType,
showSourceLocationLink = true,
onSourceLocationClick,
collapsed,
onToggleCollapsed,
id,
logEntry,
actionButtonsOverride,
openCollapseIconOverride,
}: {
headerTitle: string | React.ReactNode
level: ErrorLevel
logType?: string
sourceLocation?: SourceLocation
showSourceLocationLink?: boolean
onSourceLocationClick?: MouseEventHandler<HTMLButtonElement>
collapsed: boolean
onToggleCollapsed: () => void
id?: string
logEntry?: LogEntryData
actionButtonsOverride?: React.ReactNode
openCollapseIconOverride?: string
}) {
const { t } = useTranslation()
const logLocationSpanRef = useRef<HTMLSpanElement>(null)
const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
const { findEntityByPath } = useFileTreePathContext()
const { openEntity } = useFileTreeOpenContext()
useResizeObserver(
logLocationSpanRef,
locationSpanOverflown,
checkLocationSpanOverflow
)
const file = sourceLocation ? sourceLocation.file : null
const line = sourceLocation ? sourceLocation.line : null
const logEntryHeaderTextClasses = classNames('log-entry-header-text', {
'log-entry-header-text-error': level === 'error',
'log-entry-header-text-warning': level === 'warning',
'log-entry-header-text-info': level === 'info' || level === 'typesetting',
'log-entry-header-text-success': level === 'success',
'log-entry-header-text-raw': level === 'raw',
})
function checkLocationSpanOverflow(observedElement: ResizeObserverEntry) {
const spanEl = observedElement.target
const isOverflowing = spanEl.scrollWidth > spanEl.clientWidth
setLocationSpanOverflown(isOverflowing)
}
const locationText =
showSourceLocationLink && file ? `${file}${line ? `, ${line}` : ''}` : null
// Because we want an ellipsis on the left-hand side (e.g. "...longfilename.tex"), the
// `log-entry-location` class has text laid out from right-to-left using the CSS
// rule `direction: rtl;`.
// This works most of the times, except when the first character of the filename is considered
// a punctuation mark, like `/` (e.g. `/foo/bar/baz.sty`). In this case, because of
// right-to-left writing rules, the punctuation mark is moved to the right-side of the string,
// resulting in `...bar/baz.sty/` instead of `...bar/baz.sty`.
// To avoid this edge-case, we wrap the `logLocationLinkText` in two directional formatting
// characters:
// * \u202A LEFT-TO-RIGHT EMBEDDING Treat the following text as embedded left-to-right.
// * \u202C POP DIRECTIONAL FORMATTING End the scope of the last LRE, RLE, RLO, or LRO.
// This essentially tells the browser that, althought the text is laid out from right-to-left,
// the wrapped portion of text should follow left-to-right writing rules.
const formattedLocationText = locationText ? (
<span ref={logLocationSpanRef} className="log-entry-location">
{`\u202A${locationText}\u202C`}
</span>
) : null
const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
const fileData = useMemo(
() => file && findEntityByPath(file),
[file, findEntityByPath]
)
const showGoToCodeButton =
showSourceLocationLink &&
!!fileData &&
!(fileData.entity._id === openEntity?.entity._id && !line)
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(
event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onToggleCollapsed()
}
},
[onToggleCollapsed]
)
return (
<header className="log-entry-header-card">
<div
data-action="expand-collapse"
data-collapsed={collapsed}
className="log-entry-header-button"
onClick={onToggleCollapsed}
role="button"
tabIndex={0}
onKeyDown={handleKeyDown}
aria-label={collapsed ? t('expand') : t('collapse')}
>
<MaterialIcon
className="log-entry-expand-icon"
type={
openCollapseIconOverride ??
(collapsed ? 'chevron_right' : 'expand_more')
}
/>
<div className="log-entry-header-content">
<h3 className={logEntryHeaderTextClasses}>{headerTitleText}</h3>
{locationSpanOverflown && formattedLocationText && locationText ? (
<OLTooltip
id={locationText}
description={locationText}
overlayProps={{ placement: 'right' }}
tooltipProps={{ className: 'log-location-tooltip' }}
>
{formattedLocationText}
</OLTooltip>
) : (
formattedLocationText
)}
</div>
</div>
{actionButtonsOverride ?? (
<div className="log-entry-header-actions">
{showGoToCodeButton && (
<OLTooltip
id={`go-to-location-${locationText}`}
description={t('go_to_code_location')}
overlayProps={{ placement: 'bottom' }}
>
<OLIconButton
onClick={onSourceLocationClick}
variant="ghost"
icon="my_location"
accessibilityLabel={t('go_to_code_location')}
/>
</OLTooltip>
)}
{actionComponents.map(({ import: { default: Component }, path }) => (
<Component key={path} logEntry={logEntry} id={id} />
))}
</div>
)}
</header>
)
}
export default LogEntryHeader