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 collapsed: boolean onToggleCollapsed: () => void id?: string logEntry?: LogEntryData actionButtonsOverride?: React.ReactNode openCollapseIconOverride?: string }) { const { t } = useTranslation() const logLocationSpanRef = useRef(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 ? ( {`\u202A${locationText}\u202C`} ) : 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 = useCallback( event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() onToggleCollapsed() } }, [onToggleCollapsed] ) return (

{headerTitleText}

{locationSpanOverflown && formattedLocationText && locationText ? ( {formattedLocationText} ) : ( formattedLocationText )}
{actionButtonsOverride ?? (
{showGoToCodeButton && ( )} {actionComponents.map(({ import: { default: Component }, path }) => ( ))}
)}
) } export default LogEntryHeader