mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
Merge pull request #25939 from overleaf/dp-error-logs
Update error logs designs for new editor GitOrigin-RevId: 0de3a54446a0ff114a1debb7b5f274d3a8f19c42
This commit is contained in:
@@ -352,6 +352,7 @@ const _ProjectController = {
|
||||
'overleaf-assist-bundle',
|
||||
'word-count-client',
|
||||
'editor-popup-ux-survey',
|
||||
'new-editor-error-logs-redesign',
|
||||
].filter(Boolean)
|
||||
|
||||
const getUserValues = async userId =>
|
||||
|
||||
@@ -966,6 +966,7 @@ module.exports = {
|
||||
editorToolbarButtons: [],
|
||||
sourceEditorExtensions: [],
|
||||
sourceEditorComponents: [],
|
||||
pdfLogEntryHeaderActionComponents: [],
|
||||
pdfLogEntryComponents: [],
|
||||
pdfLogEntriesComponents: [],
|
||||
pdfPreviewPromotions: [],
|
||||
|
||||
@@ -411,7 +411,6 @@
|
||||
"discount": "",
|
||||
"discount_of": "",
|
||||
"discover_the_fastest_way_to_search_and_cite": "",
|
||||
"dismiss_error_popup": "",
|
||||
"display": "",
|
||||
"display_deleted_user": "",
|
||||
"display_math": "",
|
||||
@@ -680,6 +679,7 @@
|
||||
"go_page": "",
|
||||
"go_prev_page": "",
|
||||
"go_to_account_settings": "",
|
||||
"go_to_code_location": "",
|
||||
"go_to_code_location_in_pdf": "",
|
||||
"go_to_overleaf": "",
|
||||
"go_to_pdf_location_in_code": "",
|
||||
@@ -969,6 +969,7 @@
|
||||
"login_count": "",
|
||||
"login_to_accept_invitation": "",
|
||||
"login_with_service": "",
|
||||
"logs": "",
|
||||
"logs_and_output_files": "",
|
||||
"looking_multiple_licenses": "",
|
||||
"looks_like_youre_at": "",
|
||||
@@ -1039,6 +1040,7 @@
|
||||
"more_compile_time": "",
|
||||
"more_editor_toolbar_item": "",
|
||||
"more_info": "",
|
||||
"more_logs_and_files": "",
|
||||
"more_options": "",
|
||||
"my_library": "",
|
||||
"n_items": "",
|
||||
|
||||
BIN
Binary file not shown.
@@ -4,6 +4,7 @@
|
||||
// You may need to hard reload your browser window to see the changes.
|
||||
|
||||
export default /** @type {const} */ ([
|
||||
'auto_delete',
|
||||
'book_5',
|
||||
'brush',
|
||||
'code',
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useUserContext } from '@/shared/context/user-context'
|
||||
import { lazy, Suspense, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'classnames'
|
||||
import { RailPanelHeader } from '../rail'
|
||||
import { RailIndicator } from '../rail-indicator'
|
||||
import RailPanelHeader from '../rail-panel-header'
|
||||
|
||||
const MessageList = lazy(() => import('../../../chat/components/message-list'))
|
||||
|
||||
|
||||
+2
-12
@@ -1,9 +1,7 @@
|
||||
import PdfLogsViewer from '@/features/pdf-preview/components/pdf-logs-viewer'
|
||||
import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import { RailIndicator } from './rail-indicator'
|
||||
import { RailIndicator } from '../rail-indicator'
|
||||
|
||||
export const ErrorIndicator = () => {
|
||||
export default function ErrorIndicator() {
|
||||
const { logEntries } = useCompileContext()
|
||||
|
||||
if (!logEntries) {
|
||||
@@ -25,11 +23,3 @@ export const ErrorIndicator = () => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorPane = () => {
|
||||
return (
|
||||
<PdfPreviewProvider>
|
||||
<PdfLogsViewer alwaysVisible />
|
||||
</PdfPreviewProvider>
|
||||
)
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RailPanelHeader from '../rail-panel-header'
|
||||
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import PdfFileList from '@/features/pdf-preview/components/pdf-file-list'
|
||||
import { forwardRef } from 'react'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
|
||||
export default function ErrorLogsHeader() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<RailPanelHeader
|
||||
title={t('logs')}
|
||||
actions={[
|
||||
<DownloadFileDropdown key="download-files" />,
|
||||
<ClearCacheButton key="clear-cache" />,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ClearCacheButton = () => {
|
||||
const { compiling, clearCache, clearingCache } = useCompileContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLTooltip
|
||||
id="clear-cache"
|
||||
description={t('clear_cached_files')}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<OLIconButton
|
||||
unfilled
|
||||
onClick={() => clearCache()}
|
||||
className="rail-panel-header-button-subdued"
|
||||
icon="auto_delete"
|
||||
isLoading={clearingCache}
|
||||
disabled={clearingCache || compiling}
|
||||
accessibilityLabel={t('clear_cached_files')}
|
||||
size="sm"
|
||||
/>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const DownloadFileDropdown = () => {
|
||||
const { fileList } = useCompileContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Dropdown align="end">
|
||||
<DropdownToggle
|
||||
as={DownloadFileDropdownToggleButton}
|
||||
id="dropdown-files-dropdown-toggle"
|
||||
>
|
||||
{t('other_logs_and_files')}
|
||||
</DropdownToggle>
|
||||
{fileList && (
|
||||
<DropdownMenu id="dropdown-files-logs-pane-list">
|
||||
<PdfFileList fileList={fileList} />
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const DownloadFileDropdownToggleButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ onClick: React.MouseEventHandler }
|
||||
>(function DownloadFileDropdownToggleButton({ onClick }, ref) {
|
||||
const { compiling, fileList } = useCompileContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLTooltip
|
||||
id="more-logs-and-files"
|
||||
description={t('more_logs_and_files')}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<OLIconButton
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className="rail-panel-header-button-subdued"
|
||||
icon="download"
|
||||
disabled={compiling || !fileList}
|
||||
accessibilityLabel={t('other_logs_and_files')}
|
||||
size="sm"
|
||||
/>
|
||||
</OLTooltip>
|
||||
)
|
||||
})
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
import ErrorLogs from './error-logs'
|
||||
import ErrorLogsHeader from './error-logs-header'
|
||||
|
||||
export default function ErrorLogsPanel() {
|
||||
return (
|
||||
<PdfPreviewProvider>
|
||||
<div className="error-logs-panel">
|
||||
<ErrorLogsHeader />
|
||||
<ErrorLogs />
|
||||
</div>
|
||||
</PdfPreviewProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
import StopOnFirstErrorPrompt from '@/features/pdf-preview/components/stop-on-first-error-prompt'
|
||||
import PdfPreviewError from '@/features/pdf-preview/components/pdf-preview-error'
|
||||
import PdfValidationIssue from '@/features/pdf-preview/components/pdf-validation-issue'
|
||||
import PdfLogsEntries from '@/features/pdf-preview/components/pdf-logs-entries'
|
||||
import PdfPreviewErrorBoundaryFallback from '@/features/pdf-preview/components/pdf-preview-error-boundary-fallback'
|
||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import { Nav, NavLink, TabContainer, TabContent } from 'react-bootstrap'
|
||||
import { LogEntry as LogEntryData } from '@/features/pdf-preview/util/types'
|
||||
import LogEntry from './log-entry'
|
||||
|
||||
type ErrorLogTab = {
|
||||
key: string
|
||||
label: string
|
||||
entries: LogEntryData[] | undefined
|
||||
}
|
||||
|
||||
function ErrorLogs() {
|
||||
const { error, logEntries, rawLog, validationIssues, stoppedOnFirstError } =
|
||||
useCompileContext()
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
{ key: 'all', label: 'All', entries: logEntries?.all },
|
||||
{ key: 'errors', label: 'Errors', entries: logEntries?.errors },
|
||||
{ key: 'warnings', label: 'Warnings', entries: logEntries?.warnings },
|
||||
{ key: 'info', label: 'Info', entries: logEntries?.typesetting },
|
||||
]
|
||||
}, [logEntries])
|
||||
|
||||
const { loadingError } = usePdfPreviewContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string | null>('all')
|
||||
|
||||
const entries = useMemo(() => {
|
||||
return tabs.find(tab => tab.key === activeTab)?.entries || []
|
||||
}, [activeTab, tabs])
|
||||
|
||||
const includeErrors = activeTab === 'all' || activeTab === 'errors'
|
||||
const includeWarnings = activeTab === 'all' || activeTab === 'warnings'
|
||||
const includeInfo = activeTab === 'all' || activeTab === 'info'
|
||||
|
||||
return (
|
||||
<TabContainer onSelect={setActiveTab} defaultActiveKey={activeTab ?? 'all'}>
|
||||
<Nav defaultActiveKey="all" className="error-logs-tabs">
|
||||
{tabs.map(tab => (
|
||||
<TabHeader key={tab.key} tab={tab} active={activeTab === tab.key} />
|
||||
))}
|
||||
</Nav>
|
||||
<TabContent className="error-logs">
|
||||
<div className="logs-pane-content">
|
||||
{stoppedOnFirstError && includeErrors && <StopOnFirstErrorPrompt />}
|
||||
|
||||
{loadingError && (
|
||||
<PdfPreviewError
|
||||
error="pdf-viewer-loading-error"
|
||||
includeErrors={includeErrors}
|
||||
includeWarnings={includeWarnings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<PdfPreviewError
|
||||
error={error}
|
||||
includeErrors={includeErrors}
|
||||
includeWarnings={includeWarnings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{includeErrors &&
|
||||
validationIssues &&
|
||||
Object.entries(validationIssues).map(([name, issue]) => (
|
||||
<PdfValidationIssue key={name} name={name} issue={issue} />
|
||||
))}
|
||||
|
||||
{entries && (
|
||||
<PdfLogsEntries
|
||||
entries={entries}
|
||||
hasErrors={includeErrors && entries.length > 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{rawLog && includeInfo && (
|
||||
<LogEntry
|
||||
headerTitle={t('raw_logs')}
|
||||
rawContent={rawLog}
|
||||
entryAriaLabel={t('raw_logs_description')}
|
||||
level="raw"
|
||||
alwaysExpandRawContent
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TabContent>
|
||||
</TabContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function formatErrorNumber(num: number | undefined) {
|
||||
if (num === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (num > 99) {
|
||||
return '99+'
|
||||
}
|
||||
|
||||
return Math.floor(num).toString()
|
||||
}
|
||||
|
||||
const TabHeader = ({ tab, active }: { tab: ErrorLogTab; active: boolean }) => {
|
||||
return (
|
||||
<NavLink
|
||||
eventKey={tab.key}
|
||||
className="error-logs-tab-header"
|
||||
active={active}
|
||||
>
|
||||
{tab.label}
|
||||
<div className="error-logs-tab-count">
|
||||
{/* TODO: it would be nice if this number included custom errors */}
|
||||
{formatErrorNumber(tab.entries?.length)}
|
||||
</div>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(memo(ErrorLogs), () => (
|
||||
<PdfPreviewErrorBoundaryFallback type="logs" />
|
||||
))
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import classNames from 'classnames'
|
||||
import { useState, useRef, MouseEventHandler, ElementType } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLTooltip from '@/features/ui/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 '@/features/ui/components/ol/ol-icon-button'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
|
||||
const actionComponents = importOverleafModules(
|
||||
'pdfLogEntryHeaderActionComponents'
|
||||
) as {
|
||||
import: { default: ElementType }
|
||||
path: string
|
||||
}[]
|
||||
|
||||
function LogEntryHeader({
|
||||
sourceLocation,
|
||||
level,
|
||||
headerTitle,
|
||||
logType,
|
||||
showSourceLocationLink = true,
|
||||
onSourceLocationClick,
|
||||
collapsed,
|
||||
onToggleCollapsed,
|
||||
id,
|
||||
logEntry,
|
||||
}: {
|
||||
headerTitle: string | React.ReactNode
|
||||
level: ErrorLevel
|
||||
logType?: string
|
||||
sourceLocation?: SourceLocation
|
||||
showSourceLocationLink?: boolean
|
||||
onSourceLocationClick?: MouseEventHandler<HTMLButtonElement>
|
||||
collapsed: boolean
|
||||
onToggleCollapsed: () => void
|
||||
id?: string
|
||||
logEntry?: LogEntryData
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const logLocationSpanRef = useRef<HTMLSpanElement>(null)
|
||||
const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
|
||||
|
||||
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' || level === 'raw',
|
||||
'log-entry-header-text-success': level === 'success',
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
return (
|
||||
<header className="log-entry-header-card">
|
||||
<OLTooltip
|
||||
id={`expand-collapse-${locationText}`}
|
||||
description={collapsed ? t('expand') : t('collapse')}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<OLIconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={collapsed ? 'chevron_right' : 'expand_more'}
|
||||
onClick={onToggleCollapsed}
|
||||
accessibilityLabel={collapsed ? t('expand') : t('collapse')}
|
||||
/>
|
||||
</OLTooltip>
|
||||
<div className="log-entry-header-content">
|
||||
<h3 className={logEntryHeaderTextClasses}>{headerTitleText}</h3>
|
||||
{locationSpanOverflown && formattedLocationText && locationText ? (
|
||||
<OLTooltip
|
||||
id={locationText}
|
||||
description={locationText}
|
||||
overlayProps={{ placement: 'left' }}
|
||||
tooltipProps={{ className: 'log-location-tooltip' }}
|
||||
>
|
||||
{formattedLocationText}
|
||||
</OLTooltip>
|
||||
) : (
|
||||
formattedLocationText
|
||||
)}
|
||||
</div>
|
||||
<div className="log-entry-header-actions">
|
||||
{showSourceLocationLink && (
|
||||
<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}
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={onToggleCollapsed}
|
||||
logEntry={logEntry}
|
||||
id={id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogEntryHeader
|
||||
@@ -0,0 +1,109 @@
|
||||
import { memo, MouseEventHandler, useCallback, useState } from 'react'
|
||||
import HumanReadableLogsHints from '../../../../ide/human-readable-logs/HumanReadableLogsHints'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import {
|
||||
ErrorLevel,
|
||||
LogEntry as LogEntryData,
|
||||
SourceLocation,
|
||||
} from '@/features/pdf-preview/util/types'
|
||||
import LogEntryHeader from './log-entry-header'
|
||||
import PdfLogEntryContent from '@/features/pdf-preview/components/pdf-log-entry-content'
|
||||
|
||||
function LogEntry({
|
||||
ruleId,
|
||||
headerTitle,
|
||||
rawContent,
|
||||
logType,
|
||||
formattedContent,
|
||||
extraInfoURL,
|
||||
level,
|
||||
sourceLocation,
|
||||
showSourceLocationLink = true,
|
||||
entryAriaLabel = undefined,
|
||||
contentDetails,
|
||||
onSourceLocationClick,
|
||||
index,
|
||||
logEntry,
|
||||
id,
|
||||
alwaysExpandRawContent = false,
|
||||
}: {
|
||||
headerTitle: string | React.ReactNode
|
||||
level: ErrorLevel
|
||||
ruleId?: string
|
||||
rawContent?: string
|
||||
logType?: string
|
||||
formattedContent?: React.ReactNode
|
||||
extraInfoURL?: string | null
|
||||
sourceLocation?: SourceLocation
|
||||
showSourceLocationLink?: boolean
|
||||
entryAriaLabel?: string
|
||||
contentDetails?: string[]
|
||||
onSourceLocationClick?: (sourceLocation: SourceLocation) => void
|
||||
index?: number
|
||||
logEntry?: LogEntryData
|
||||
id?: string
|
||||
alwaysExpandRawContent?: boolean
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
if (ruleId && HumanReadableLogsHints[ruleId]) {
|
||||
const hint = HumanReadableLogsHints[ruleId]
|
||||
formattedContent = hint.formattedContent(contentDetails)
|
||||
extraInfoURL = hint.extraInfoURL
|
||||
}
|
||||
|
||||
const handleLogEntryLinkClick: MouseEventHandler<HTMLButtonElement> =
|
||||
useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
|
||||
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
|
||||
className="log-entry"
|
||||
data-ruleid={ruleId}
|
||||
data-log-entry-id={id}
|
||||
aria-label={entryAriaLabel}
|
||||
>
|
||||
<LogEntryHeader
|
||||
level={level}
|
||||
sourceLocation={sourceLocation}
|
||||
headerTitle={headerTitle}
|
||||
logType={logType}
|
||||
showSourceLocationLink={showSourceLocationLink}
|
||||
onSourceLocationClick={handleLogEntryLinkClick}
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={() => setCollapsed(collapsed => !collapsed)}
|
||||
id={id}
|
||||
logEntry={logEntry}
|
||||
/>
|
||||
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div className="horizontal-divider" />
|
||||
<PdfLogEntryContent
|
||||
alwaysExpandRawContent={alwaysExpandRawContent}
|
||||
rawContent={rawContent}
|
||||
formattedContent={formattedContent}
|
||||
extraInfoURL={extraInfoURL}
|
||||
index={index}
|
||||
logEntry={logEntry}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(LogEntry)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import PdfLogsViewer from '@/features/pdf-preview/components/pdf-logs-viewer'
|
||||
import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
|
||||
export default function OldErrorPane() {
|
||||
return (
|
||||
<PdfPreviewProvider>
|
||||
<PdfLogsViewer alwaysVisible />
|
||||
</PdfPreviewProvider>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { ElementType } from 'react'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import { RailPanelHeader } from '../rail'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RailPanelHeader from '../rail-panel-header'
|
||||
|
||||
const integrationPanelComponents = importOverleafModules(
|
||||
'integrationPanelComponents'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRailContext } from '../contexts/rail-context'
|
||||
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
|
||||
import React from 'react'
|
||||
|
||||
export default function RailPanelHeader({
|
||||
title,
|
||||
actions,
|
||||
}: {
|
||||
title: string
|
||||
actions?: React.ReactNode[]
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { handlePaneCollapse } = useRailContext()
|
||||
return (
|
||||
<header className="rail-panel-header">
|
||||
<h4 className="rail-panel-title">{title}</h4>
|
||||
|
||||
<div className="rail-panel-header-actions">
|
||||
{actions}
|
||||
<OLIconButton
|
||||
onClick={handlePaneCollapse}
|
||||
className="rail-panel-header-button-subdued"
|
||||
icon="close"
|
||||
accessibilityLabel={t('close')}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import MaterialIcon, {
|
||||
} from '@/shared/components/material-icon'
|
||||
import { Panel } from 'react-resizable-panels'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { ErrorIndicator, ErrorPane } from './errors'
|
||||
import ErrorIndicator from './error-logs/error-indicator'
|
||||
import {
|
||||
RailModalKey,
|
||||
RailTabKey,
|
||||
@@ -39,6 +39,10 @@ import {
|
||||
hasFullProjectSearch,
|
||||
} from './full-project-search-panel'
|
||||
import { sendSearchEvent } from '@/features/event-tracking/search-events'
|
||||
import ErrorLogsPanel from './error-logs/error-logs-panel'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import OldErrorPane from './error-logs/old-error-pane'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
type RailElement = {
|
||||
icon: AvailableUnfilledIcon
|
||||
@@ -47,6 +51,7 @@ type RailElement = {
|
||||
indicator?: ReactElement
|
||||
title: string
|
||||
hide?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type RailActionButton = {
|
||||
@@ -96,6 +101,8 @@ export const RailLayout = () => {
|
||||
togglePane,
|
||||
setResizing,
|
||||
} = useRailContext()
|
||||
const { logEntries } = useCompileContext()
|
||||
const errorLogsDisabled = !logEntries
|
||||
|
||||
const { view, setLeftMenuShown } = useLayoutContext()
|
||||
|
||||
@@ -103,6 +110,8 @@ export const RailLayout = () => {
|
||||
|
||||
const isHistoryView = view === 'history'
|
||||
|
||||
const newErrorlogs = useFeatureFlag('new-editor-error-logs-redesign')
|
||||
|
||||
const railTabs: RailElement[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -142,11 +151,12 @@ export const RailLayout = () => {
|
||||
key: 'errors',
|
||||
icon: 'report',
|
||||
title: t('error_log'),
|
||||
component: <ErrorPane />,
|
||||
component: newErrorlogs ? <ErrorLogsPanel /> : <OldErrorPane />,
|
||||
indicator: <ErrorIndicator />,
|
||||
disabled: errorLogsDisabled,
|
||||
},
|
||||
],
|
||||
[t]
|
||||
[t, errorLogsDisabled, newErrorlogs]
|
||||
)
|
||||
|
||||
const railActions: RailAction[] = useMemo(
|
||||
@@ -217,7 +227,7 @@ export const RailLayout = () => {
|
||||
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
||||
{railTabs
|
||||
.filter(({ hide }) => !hide)
|
||||
.map(({ icon, key, indicator, title }) => (
|
||||
.map(({ icon, key, indicator, title, disabled }) => (
|
||||
<RailTab
|
||||
open={isOpen && selectedTab === key}
|
||||
key={key}
|
||||
@@ -225,6 +235,7 @@ export const RailLayout = () => {
|
||||
icon={icon}
|
||||
indicator={indicator}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
<div className="flex-grow-1" />
|
||||
@@ -251,7 +262,7 @@ export const RailLayout = () => {
|
||||
hidden: isHistoryView,
|
||||
})}
|
||||
>
|
||||
<Tab.Content>
|
||||
<Tab.Content className="ide-rail-tab-content">
|
||||
{railTabs
|
||||
.filter(({ hide }) => !hide)
|
||||
.map(({ key, component }) => (
|
||||
@@ -291,12 +302,14 @@ const RailTab = ({
|
||||
open,
|
||||
indicator,
|
||||
title,
|
||||
disabled = false,
|
||||
}: {
|
||||
icon: AvailableUnfilledIcon
|
||||
eventKey: string
|
||||
open: boolean
|
||||
indicator?: ReactElement
|
||||
title: string
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<OLTooltip
|
||||
@@ -309,6 +322,7 @@ const RailTab = ({
|
||||
className={classNames('ide-rail-tab-link', {
|
||||
'open-rail': open,
|
||||
})}
|
||||
disabled={disabled}
|
||||
>
|
||||
{open ? (
|
||||
<MaterialIcon
|
||||
@@ -382,23 +396,6 @@ const RailActionElement = ({ action }: { action: RailAction }) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const RailPanelHeader: FC<{ title: string }> = ({ title }) => {
|
||||
const { t } = useTranslation()
|
||||
const { handlePaneCollapse } = useRailContext()
|
||||
return (
|
||||
<header className="rail-panel-header">
|
||||
<h4 className="rail-panel-title">{title}</h4>
|
||||
<OLIconButton
|
||||
onClick={handlePaneCollapse}
|
||||
className="rail-panel-header-button-subdued"
|
||||
icon="close"
|
||||
accessibilityLabel={t('close')}
|
||||
size="sm"
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
const RailHelpDropdown = () => {
|
||||
const showSupport = getMeta('ol-showSupport')
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -17,12 +17,14 @@ export default function PdfLogEntryContent({
|
||||
extraInfoURL,
|
||||
index,
|
||||
logEntry,
|
||||
alwaysExpandRawContent = false,
|
||||
}: {
|
||||
rawContent?: string
|
||||
formattedContent?: React.ReactNode
|
||||
extraInfoURL?: string | null
|
||||
index?: number
|
||||
logEntry?: LogEntry
|
||||
alwaysExpandRawContent?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -48,7 +50,11 @@ export default function PdfLogEntryContent({
|
||||
)}
|
||||
|
||||
{rawContent && (
|
||||
<PdfLogEntryRawContent rawContent={rawContent} collapsedSize={150} />
|
||||
<PdfLogEntryRawContent
|
||||
rawContent={rawContent}
|
||||
collapsedSize={150}
|
||||
alwaysExpanded={alwaysExpandRawContent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
+8
-4
@@ -8,20 +8,24 @@ import Icon from '../../../shared/components/icon'
|
||||
export default function PdfLogEntryRawContent({
|
||||
rawContent,
|
||||
collapsedSize = 0,
|
||||
alwaysExpanded = false,
|
||||
}: {
|
||||
rawContent: string
|
||||
collapsedSize?: number
|
||||
alwaysExpanded?: boolean
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [needsExpander, setNeedsExpander] = useState(true)
|
||||
const [expanded, setExpanded] = useState(alwaysExpanded)
|
||||
const [needsExpander, setNeedsExpander] = useState(!alwaysExpanded)
|
||||
|
||||
const { elementRef } = useResizeObserver(
|
||||
useCallback(
|
||||
(element: Element) => {
|
||||
if (element.scrollHeight === 0) return // skip update when logs-pane is closed
|
||||
setNeedsExpander(element.scrollHeight > collapsedSize)
|
||||
setNeedsExpander(
|
||||
!alwaysExpanded && element.scrollHeight > collapsedSize
|
||||
)
|
||||
},
|
||||
[collapsedSize]
|
||||
[collapsedSize, alwaysExpanded]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import classNames from 'classnames'
|
||||
import { memo, MouseEventHandler, useCallback } from 'react'
|
||||
import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-header'
|
||||
import PdfLogEntryContent from './pdf-log-entry-content'
|
||||
@@ -6,6 +5,9 @@ import HumanReadableLogsHints from '../../../ide/human-readable-logs/HumanReadab
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { ErrorLevel, LogEntry, SourceLocation } from '../util/types'
|
||||
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
|
||||
import NewLogEntry from '@/features/ide-redesign/components/error-logs/log-entry'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function PdfLogEntry({
|
||||
ruleId,
|
||||
@@ -18,12 +20,9 @@ function PdfLogEntry({
|
||||
level,
|
||||
sourceLocation,
|
||||
showSourceLocationLink = true,
|
||||
showCloseButton = false,
|
||||
entryAriaLabel = undefined,
|
||||
customClass,
|
||||
contentDetails,
|
||||
onSourceLocationClick,
|
||||
onClose,
|
||||
index,
|
||||
logEntry,
|
||||
id,
|
||||
@@ -38,12 +37,9 @@ function PdfLogEntry({
|
||||
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
|
||||
@@ -73,9 +69,34 @@ function PdfLogEntry({
|
||||
[level, onSourceLocationClick, ruleId, sourceLocation]
|
||||
)
|
||||
|
||||
const newEditor = useIsNewEditorEnabled()
|
||||
const newErrorlogs = useFeatureFlag('new-editor-error-logs-redesign')
|
||||
|
||||
if (newEditor && newErrorlogs) {
|
||||
return (
|
||||
<NewLogEntry
|
||||
index={index}
|
||||
id={id}
|
||||
logEntry={logEntry}
|
||||
ruleId={ruleId}
|
||||
headerTitle={headerTitle}
|
||||
formattedContent={formattedContent}
|
||||
rawContent={rawContent}
|
||||
logType={logType}
|
||||
level={level}
|
||||
contentDetails={contentDetails}
|
||||
entryAriaLabel={entryAriaLabel}
|
||||
sourceLocation={sourceLocation}
|
||||
onSourceLocationClick={onSourceLocationClick}
|
||||
showSourceLocationLink={showSourceLocationLink}
|
||||
extraInfoURL={extraInfoURL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('log-entry', customClass)}
|
||||
className="log-entry"
|
||||
aria-label={entryAriaLabel}
|
||||
data-ruleid={ruleId}
|
||||
data-log-entry-id={id}
|
||||
@@ -88,8 +109,6 @@ function PdfLogEntry({
|
||||
logType={logType}
|
||||
showSourceLocationLink={showSourceLocationLink}
|
||||
onSourceLocationClick={handleLogEntryLinkClick}
|
||||
showCloseButton={showCloseButton}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
{(rawContent || formattedContent || showAiErrorAssistant) && (
|
||||
|
||||
@@ -6,7 +6,15 @@ 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 }: { error: string }) {
|
||||
function PdfPreviewError({
|
||||
error,
|
||||
includeWarnings = true,
|
||||
includeErrors = true,
|
||||
}: {
|
||||
error: string
|
||||
includeWarnings?: boolean
|
||||
includeErrors?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { startCompile } = useCompileContext()
|
||||
@@ -14,191 +22,217 @@ function PdfPreviewError({ error }: { error: string }) {
|
||||
switch (error) {
|
||||
case 'rendering-error-expected':
|
||||
return (
|
||||
<PdfLogEntry
|
||||
headerTitle={t('pdf_rendering_error')}
|
||||
formattedContent={
|
||||
<>
|
||||
<Trans
|
||||
i18nKey="something_went_wrong_rendering_pdf_expected"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<OLButton
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => startCompile()}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<Trans
|
||||
i18nKey="last_resort_trouble_shooting_guide"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
level="warning"
|
||||
/>
|
||||
includeWarnings && (
|
||||
<PdfLogEntry
|
||||
headerTitle={t('pdf_rendering_error')}
|
||||
formattedContent={
|
||||
<>
|
||||
<Trans
|
||||
i18nKey="something_went_wrong_rendering_pdf_expected"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<OLButton
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => startCompile()}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<Trans
|
||||
i18nKey="last_resort_trouble_shooting_guide"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
level="warning"
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
case 'rendering-error':
|
||||
return (
|
||||
<ErrorLogEntry title={t('pdf_rendering_error')}>
|
||||
{t('something_went_wrong_rendering_pdf')}
|
||||
|
||||
<Trans
|
||||
i18nKey="try_recompile_project_or_troubleshoot"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
{getMeta('ol-compilesUserContentDomain') && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
<Trans
|
||||
i18nKey="new_compile_domain_notice"
|
||||
values={{
|
||||
compilesUserContentDomain: new URL(
|
||||
getMeta('ol-compilesUserContentDomain')
|
||||
).hostname,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
<code key="domain" />,
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('pdf_rendering_error')}>
|
||||
{t('something_went_wrong_rendering_pdf')}
|
||||
|
||||
<Trans
|
||||
i18nKey="try_recompile_project_or_troubleshoot"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
{getMeta('ol-compilesUserContentDomain') && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
<Trans
|
||||
i18nKey="new_compile_domain_notice"
|
||||
values={{
|
||||
compilesUserContentDomain: new URL(
|
||||
getMeta('ol-compilesUserContentDomain')
|
||||
).hostname,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
<code key="domain" />,
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'clsi-maintenance':
|
||||
return (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('clsi_maintenance')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('clsi_maintenance')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'clsi-unavailable':
|
||||
return (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('clsi_unavailable')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('clsi_unavailable')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'too-recently-compiled':
|
||||
return (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('too_recently_compiled')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('too_recently_compiled')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'terminated':
|
||||
return (
|
||||
<ErrorLogEntry title={t('terminated')}>
|
||||
{t('compile_terminated_by_user')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('terminated')}>
|
||||
{t('compile_terminated_by_user')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'rate-limited':
|
||||
return (
|
||||
<ErrorLogEntry title={t('pdf_compile_rate_limit_hit')}>
|
||||
{t('project_flagged_too_many_compiles')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('pdf_compile_rate_limit_hit')}>
|
||||
{t('project_flagged_too_many_compiles')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'compile-in-progress':
|
||||
return (
|
||||
<ErrorLogEntry title={t('pdf_compile_in_progress_error')}>
|
||||
{t('pdf_compile_try_again')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('pdf_compile_in_progress_error')}>
|
||||
{t('pdf_compile_try_again')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'autocompile-disabled':
|
||||
return (
|
||||
<ErrorLogEntry title={t('autocompile_disabled')}>
|
||||
{t('autocompile_disabled_reason')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('autocompile_disabled')}>
|
||||
{t('autocompile_disabled_reason')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'project-too-large':
|
||||
return (
|
||||
<ErrorLogEntry title={t('project_too_large')}>
|
||||
{t('project_too_much_editable_text')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('project_too_large')}>
|
||||
{t('project_too_much_editable_text')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'timedout':
|
||||
return <TimedOutLogEntry />
|
||||
return includeErrors && <TimedOutLogEntry />
|
||||
|
||||
case 'failure':
|
||||
return (
|
||||
<ErrorLogEntry title={t('no_pdf_error_title')}>
|
||||
{t('no_pdf_error_explanation')}
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('no_pdf_error_title')}>
|
||||
{t('no_pdf_error_explanation')}
|
||||
|
||||
<ul className="my-1 ps-3">
|
||||
<li>{t('no_pdf_error_reason_unrecoverable_error')}</li>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey="no_pdf_error_reason_no_content"
|
||||
components={{ code: <code /> }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey="no_pdf_error_reason_output_pdf_already_exists"
|
||||
components={{ code: <code /> }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</ErrorLogEntry>
|
||||
<ul className="my-1 ps-3">
|
||||
<li>{t('no_pdf_error_reason_unrecoverable_error')}</li>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey="no_pdf_error_reason_no_content"
|
||||
components={{ code: <code /> }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey="no_pdf_error_reason_output_pdf_already_exists"
|
||||
components={{ code: <code /> }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'clear-cache':
|
||||
return (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('somthing_went_wrong_compiling')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('somthing_went_wrong_compiling')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'pdf-viewer-loading-error':
|
||||
return (
|
||||
<ErrorLogEntry title={t('pdf_rendering_error')}>
|
||||
<Trans
|
||||
i18nKey="something_went_wrong_loading_pdf_viewer"
|
||||
components={[
|
||||
<strong key="strong-" />,
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a key="contact-link" target="_blank" href="/contact" />,
|
||||
]}
|
||||
/>
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('pdf_rendering_error')}>
|
||||
<Trans
|
||||
i18nKey="something_went_wrong_loading_pdf_viewer"
|
||||
components={[
|
||||
<strong key="strong-" />,
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
|
||||
target="_blank"
|
||||
key="troubleshooting-link"
|
||||
/>,
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a key="contact-link" target="_blank" href="/contact" />,
|
||||
]}
|
||||
/>
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
|
||||
case 'validation-problems':
|
||||
@@ -207,9 +241,11 @@ function PdfPreviewError({ error }: { error: string }) {
|
||||
case 'error':
|
||||
default:
|
||||
return (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('somthing_went_wrong_compiling')}
|
||||
</ErrorLogEntry>
|
||||
includeErrors && (
|
||||
<ErrorLogEntry title={t('server_error')}>
|
||||
{t('somthing_went_wrong_compiling')}
|
||||
</ErrorLogEntry>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -17,7 +17,7 @@ type Segmentation = Record<
|
||||
>
|
||||
|
||||
interface TimeoutUpgradePaywallPromptProps {
|
||||
setIsShowingPrimary: Dispatch<SetStateAction<boolean>>
|
||||
setIsShowingPrimary?: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
function TimeoutUpgradePaywallPrompt({
|
||||
@@ -40,7 +40,9 @@ function TimeoutUpgradePaywallPrompt({
|
||||
function onClose() {
|
||||
sendPaywallEvent('paywall-dismiss')
|
||||
setIsPaywallDismissed(true)
|
||||
setIsShowingPrimary(false)
|
||||
if (setIsShowingPrimary) {
|
||||
setIsShowingPrimary(false)
|
||||
}
|
||||
}
|
||||
|
||||
function onClickInfoLink() {
|
||||
@@ -57,7 +59,9 @@ function TimeoutUpgradePaywallPrompt({
|
||||
sendPaywallEvent('paywall-prompt', {
|
||||
plan: isStudent ? 'student' : 'collaborator',
|
||||
})
|
||||
setIsShowingPrimary(true)
|
||||
if (setIsShowingPrimary) {
|
||||
setIsShowingPrimary(true)
|
||||
}
|
||||
}, [isStudent, setIsShowingPrimary])
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,9 +14,7 @@ function PreviewLogEntryHeader({
|
||||
headerIcon,
|
||||
logType,
|
||||
showSourceLocationLink = true,
|
||||
showCloseButton = false,
|
||||
onSourceLocationClick,
|
||||
onClose,
|
||||
}: {
|
||||
headerTitle: string | React.ReactNode
|
||||
level: ErrorLevel
|
||||
@@ -24,9 +22,7 @@ function PreviewLogEntryHeader({
|
||||
logType?: string
|
||||
sourceLocation?: SourceLocation
|
||||
showSourceLocationLink?: boolean
|
||||
showCloseButton?: boolean
|
||||
onSourceLocationClick?: MouseEventHandler<HTMLButtonElement>
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const logLocationSpanRef = useRef<HTMLSpanElement>(null)
|
||||
@@ -116,16 +112,6 @@ function PreviewLogEntryHeader({
|
||||
) : (
|
||||
locationLink
|
||||
)}
|
||||
{showCloseButton ? (
|
||||
<OLButton
|
||||
variant="link"
|
||||
className="btn-inline-link log-entry-header-link"
|
||||
aria-label={t('dismiss_error_popup')}
|
||||
onClick={onClose}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</OLButton>
|
||||
) : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
PdfScrollPosition,
|
||||
usePdfScrollPosition,
|
||||
} from '@/shared/hooks/use-pdf-scroll-position'
|
||||
import { PdfFileDataList } from '@/features/pdf-preview/util/types'
|
||||
import { LogEntry, PdfFileDataList } from '@/features/pdf-preview/util/types'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
import OError from '@overleaf/o-error'
|
||||
@@ -69,7 +69,12 @@ export type CompileContext = {
|
||||
hasShortCompileTimeout: boolean
|
||||
highlights?: Record<string, any>[]
|
||||
isProjectOwner: boolean
|
||||
logEntries?: Record<string, any>
|
||||
logEntries?: {
|
||||
all: LogEntry[]
|
||||
errors: LogEntry[]
|
||||
warnings: LogEntry[]
|
||||
typesetting: LogEntry[]
|
||||
}
|
||||
logEntryAnnotations?: Record<string, any>
|
||||
outputFilesArchive?: string
|
||||
pdfDownloadUrl?: string
|
||||
|
||||
@@ -34,7 +34,6 @@ const fakeArgs = {
|
||||
formattedContent: 'This is a log entry',
|
||||
level: 'error' as const,
|
||||
extraInfoURL: 'https://example.com',
|
||||
showCloseButton: true,
|
||||
showSourceLocationLink: true,
|
||||
rawContent: 'This is a raw log entry',
|
||||
contentDetails: ['detail 1', 'detail 2'],
|
||||
|
||||
@@ -1,16 +1,168 @@
|
||||
:root {
|
||||
--logs-pane-bg: var(--bg-dark-secondary);
|
||||
}
|
||||
|
||||
.ide-redesign-main {
|
||||
--logs-pane-bg: var(--bg-dark-primary);
|
||||
--logs-info-color: var(--blue-40);
|
||||
}
|
||||
|
||||
@include theme('light') {
|
||||
--logs-pane-bg: var(--bg-light-secondary);
|
||||
--logs-info-color: var(--blue-60);
|
||||
}
|
||||
|
||||
.ide-redesign-main {
|
||||
--logs-pane-bg: #fff;
|
||||
.ide-redesign-main {
|
||||
--logs-pane-bg: var(--bg-primary-themed);
|
||||
|
||||
.error-logs-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// TODO $ide-redesign-cleanup: Unnest everything from inside .error-logs
|
||||
// when we remove the styles for the old pane
|
||||
.error-logs {
|
||||
--logs-pane-bg: var(--bg-secondary-themed);
|
||||
|
||||
overflow-y: auto;
|
||||
background-color: var(--logs-pane-bg);
|
||||
flex: 1;
|
||||
|
||||
.logs-pane-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-02);
|
||||
gap: var(--spacing-02);
|
||||
}
|
||||
|
||||
.icon-button,
|
||||
.icon-button-small {
|
||||
@include ol-button-variant(
|
||||
var(--content-secondary-themed),
|
||||
transparent,
|
||||
transparent,
|
||||
var(--bg-secondary-themed),
|
||||
transparent,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
.log-entry-content {
|
||||
color: var(--content-primary-themed);
|
||||
background-color: var(--bg-primary-themed);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.log-entry-content-raw {
|
||||
color: var(--content-primary-themed);
|
||||
background-color: var(--bg-secondary-themed);
|
||||
}
|
||||
|
||||
.log-entry-content-raw-container {
|
||||
background-color: var(--bg-secondary-themed);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
background-color: var(--bg-primary-themed);
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: var(--spacing-05) var(--spacing-04);
|
||||
|
||||
.horizontal-divider {
|
||||
margin: var(--spacing-04) 0;
|
||||
border-top: 1px solid var(--border-divider-themed);
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-header-card {
|
||||
background-color: var(--bg-primary-themed);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: var(--spacing-03);
|
||||
align-items: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.log-entry-header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
gap: var(--spacing-02);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-entry-header-text {
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-02);
|
||||
line-height: var(--line-height-02);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.log-entry-header-text-error {
|
||||
color: var(--content-danger-themed);
|
||||
}
|
||||
|
||||
.log-entry-header-text-warning {
|
||||
color: var(--content-warning-themed);
|
||||
}
|
||||
|
||||
.log-entry-header-text-info {
|
||||
color: var(--logs-info-color);
|
||||
}
|
||||
|
||||
.log-entry-header-text-success {
|
||||
color: var(--content-positive-themed);
|
||||
}
|
||||
|
||||
.log-entry-location {
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--font-size-01);
|
||||
line-height: var(--line-height-01);
|
||||
color: var(--content-secondary-themed);
|
||||
}
|
||||
|
||||
.log-entry-header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-03);
|
||||
}
|
||||
|
||||
.log-entry-content-button-container {
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
var(--bg-secondary-themed) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.error-logs-tabs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.error-logs-tab-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-04);
|
||||
color: var(--content-secondary-themed);
|
||||
padding: var(--spacing-03) var(--spacing-04);
|
||||
font-size: var(--font-size-02);
|
||||
border-bottom: 3px solid transparent;
|
||||
|
||||
&[aria-selected='true'] {
|
||||
color: var(--content-primary-themed);
|
||||
border-bottom: 3px solid var(--bg-accent-01);
|
||||
}
|
||||
}
|
||||
|
||||
.error-logs-tab-count {
|
||||
background-color: var(--bg-secondary-themed);
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 var(--spacing-02);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,53 +300,53 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-content {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
background-color: var(--bg-light-primary);
|
||||
color: var(--content-primary);
|
||||
padding: var(--spacing-04);
|
||||
.log-entry-content {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-entry-content-raw {
|
||||
@include body-xs;
|
||||
background-color: var(--bg-light-primary);
|
||||
color: var(--content-primary);
|
||||
padding: var(--spacing-04);
|
||||
}
|
||||
|
||||
color: var(--content-secondary);
|
||||
padding: var(--spacing-03);
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.log-entry-content-raw {
|
||||
@include body-xs;
|
||||
|
||||
.log-entry-content-button-container {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-top: 0;
|
||||
transition:
|
||||
margin 0.15s ease-in-out,
|
||||
opacity 0.15s ease-in-out;
|
||||
padding-bottom: var(--spacing-04);
|
||||
text-align: center;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
var(--bg-light-tertiary) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||
}
|
||||
color: var(--content-secondary);
|
||||
padding: var(--spacing-03);
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.log-entry-content-button-container-collapsed {
|
||||
margin-top: -40px;
|
||||
}
|
||||
.log-entry-content-button-container {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-top: 0;
|
||||
transition:
|
||||
margin 0.15s ease-in-out,
|
||||
opacity 0.15s ease-in-out;
|
||||
padding-bottom: var(--spacing-04);
|
||||
text-align: center;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
var(--bg-light-tertiary) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||
}
|
||||
|
||||
.log-entry-content-raw-container {
|
||||
background-color: var(--bg-light-tertiary);
|
||||
border-radius: var(--border-radius-base);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-03);
|
||||
}
|
||||
.log-entry-content-button-container-collapsed {
|
||||
margin-top: -40px;
|
||||
}
|
||||
|
||||
.log-entry-content-raw-container {
|
||||
background-color: var(--bg-light-tertiary);
|
||||
border-radius: var(--border-radius-base);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-03);
|
||||
}
|
||||
|
||||
.log-entry-formatted-content,
|
||||
|
||||
@@ -35,6 +35,12 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.rail-panel-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-01);
|
||||
}
|
||||
|
||||
.rail-panel-title {
|
||||
font-size: var(--font-size-02);
|
||||
color: var(--ide-rail-color);
|
||||
@@ -127,12 +133,12 @@ body {
|
||||
.tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.ide-rail-tab-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ide-rail-tabs-nav {
|
||||
|
||||
@@ -432,7 +432,6 @@
|
||||
"disconnected": "Forbindelsen blev afbrudt",
|
||||
"discount_of": "Rabat på __amount__",
|
||||
"discover_latex_templates_and_examples": "Opdag LaTeX skabeloner og eksempler til at hjælpe med alt fra at skrive en artikel til at bruge en specifik LaTeX pakke.",
|
||||
"dismiss_error_popup": "Afvis første fejlmeddelelse",
|
||||
"display_deleted_user": "Vis slettede brugere",
|
||||
"do_not_have_acct_or_do_not_want_to_link": "Hvis du ikke har en <b>__appName__</b>-konto, eller hvis du ikke vil kæde den sammen med din <b>__institutionName__</b>-konto, klik venligst <b>__clickText__</b>.",
|
||||
"do_not_link_accounts": "Kæd ikke kontoer sammen",
|
||||
|
||||
@@ -312,7 +312,6 @@
|
||||
"disable_stop_on_first_error": "„Anhalten beim ersten Fehler“ deaktivieren",
|
||||
"disconnected": "Nicht verbunden",
|
||||
"discount_of": "__amount__ Rabatt",
|
||||
"dismiss_error_popup": "Erste Fehlermeldung schließen",
|
||||
"do_not_have_acct_or_do_not_want_to_link": "Wenn du kein <b>__appName__</b>-Konto hast oder nicht mit deinem <b>__institutionName__</b>-Konto verknüpfen möchtest, klicke auf <b>„__clickText__“</b>.",
|
||||
"do_not_link_accounts": "Konten nicht verknüpfen",
|
||||
"do_you_want_to_change_your_primary_email_address_to": "Willst Du deine primäre E-Mail-Adresse in <b>__email__</b> ändern?",
|
||||
|
||||
@@ -536,7 +536,6 @@
|
||||
"discover_latex_templates_and_examples": "Discover LaTeX templates and examples to help with everything from writing a journal article to using a specific LaTeX package.",
|
||||
"discover_the_fastest_way_to_search_and_cite": "Discover the fastest way to search and cite",
|
||||
"discover_why_over_people_worldwide_trust_overleaf": "Discover why over __count__ million people worldwide trust Overleaf with their work.",
|
||||
"dismiss_error_popup": "Dismiss first error alert",
|
||||
"display": "Display",
|
||||
"display_deleted_user": "Display deleted users",
|
||||
"display_math": "Display math",
|
||||
@@ -889,6 +888,7 @@
|
||||
"go_page": "Go to page __page__",
|
||||
"go_prev_page": "Go to Previous Page",
|
||||
"go_to_account_settings": "Go to account settings",
|
||||
"go_to_code_location": "Go to code location",
|
||||
"go_to_code_location_in_pdf": "Go to code location in PDF",
|
||||
"go_to_first_page": "Go to first page",
|
||||
"go_to_last_page": "Go to last page",
|
||||
@@ -1273,6 +1273,7 @@
|
||||
"login_to_overleaf": "Log in to Overleaf",
|
||||
"login_with_service": "Log in with __service__",
|
||||
"login_with_sso": "Log in to Overleaf with SSO",
|
||||
"logs": "Logs",
|
||||
"logs_and_output_files": "Logs and output files",
|
||||
"longer_compile_timeout": "Longer <0>compile timeout</0>",
|
||||
"longer_compile_timeout_on_faster_servers": "Longer compile timeout on faster servers",
|
||||
@@ -1357,6 +1358,7 @@
|
||||
"more_compile_time": "More compile time",
|
||||
"more_editor_toolbar_item": "More editor toolbar items",
|
||||
"more_info": "More Info",
|
||||
"more_logs_and_files": "More logs and files",
|
||||
"more_options": "More options",
|
||||
"more_project_collaborators": "<0>More</0> project <0>collaborators</0>",
|
||||
"more_than_one_kind_of_snippet_was_requested": "The link to open this content on Overleaf included some invalid parameters. If this keeps happening for links on a particular site, please report this to them.",
|
||||
|
||||
@@ -344,7 +344,6 @@
|
||||
"disable_stop_on_first_error": "Désactiver “Arrêter à la première erreur”",
|
||||
"disconnected": "Déconnecté",
|
||||
"discount_of": "Remise de __amount__",
|
||||
"dismiss_error_popup": "Ignorer l’alerte de première erreur",
|
||||
"do_not_have_acct_or_do_not_want_to_link": "Si vous n’avez pas de compte <b>__appName__</b> ou si vous ne souhaitez pas le lier à votre compte <b>__institutionName__</b>, veuillez cliquer <b>__clickText__</b>.",
|
||||
"do_not_link_accounts": "Ne pas lier les comptes",
|
||||
"do_you_want_to_change_your_primary_email_address_to": "Voulez-vous définir <b>__email__</b> comme votre adresse email principale ?",
|
||||
|
||||
@@ -208,7 +208,6 @@
|
||||
"dictionary": "Ordbok",
|
||||
"disable_stop_on_first_error": "Inaktivera \"Stopp vid första fel\"",
|
||||
"disconnected": "Frånkopplad",
|
||||
"dismiss_error_popup": "Avfärda varning om första fel",
|
||||
"do_not_have_acct_or_do_not_want_to_link": "Om du inte har ett <b>__appName__</b>-konto, eller om du inte vill länka till ditt <b>__institutionName__</b>-konto, vänligen klicka på <b>__clickText__</b>.",
|
||||
"do_not_link_accounts": "Länka ej konton",
|
||||
"documentation": "Dokumentation",
|
||||
|
||||
@@ -518,7 +518,6 @@
|
||||
"discover_latex_templates_and_examples": "探索 LaTeX 模板和示例,以帮助完成从撰写期刊文章到使用特定 LaTeX 包的所有工作。",
|
||||
"discover_the_fastest_way_to_search_and_cite": "探索搜索和引用的最快方法",
|
||||
"discover_why_over_people_worldwide_trust_overleaf": "了解为什么全世界有超过__count__万人信任 Overleaf 并把工作交给它。",
|
||||
"dismiss_error_popup": "忽略第一个错误提示",
|
||||
"display": "显示",
|
||||
"display_deleted_user": "显示已删除的用户",
|
||||
"display_math": "显示数学公式",
|
||||
|
||||
Reference in New Issue
Block a user