Merge pull request #25939 from overleaf/dp-error-logs

Update error logs designs for new editor

GitOrigin-RevId: 0de3a54446a0ff114a1debb7b5f274d3a8f19c42
This commit is contained in:
David
2025-06-05 08:05:11 +00:00
committed by Copybot
parent a134a2b799
commit db98f5132b
32 changed files with 1021 additions and 267 deletions
@@ -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 =>
+1
View File
@@ -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": "",
@@ -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'))
@@ -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>
)
}
@@ -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>
)
})
@@ -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" />
))
@@ -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)
@@ -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,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,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')}
&nbsp;
<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')}
&nbsp;
<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>
)
)
}
}
@@ -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">&times;</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 {
-1
View File
@@ -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",
-1
View File
@@ -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?",
+3 -1
View File
@@ -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.",
-1
View File
@@ -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 ?",
-1
View File
@@ -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",
-1
View File
@@ -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": "显示数学公式",