mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Add error and validation issues (#3400)
* Remove references to the duplicatePaths validation * Make the log entries more generic, to support validation and CLSI errors * Add validation issues to the new logs UI * Add CLSI errors to the new logs UI * Update tests; accessibility fixes * Disable PDF viewing when compile fails; address PR feedback. * Add accessible description for error and validation failed compiles GitOrigin-RevId: 8b0597af8857712d47c20e4915470e8e745bb315
This commit is contained in:
parent
64e19085b4
commit
081f4212a8
20 changed files with 771 additions and 342 deletions
|
@ -8,8 +8,11 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
isDraftModeOn: draft,
|
||||
isSyntaxCheckOn: stop_on_validation_error,
|
||||
lastCompileTimestamp: pdf.lastCompileTimestamp,
|
||||
logEntries: pdf.logEntries ? pdf.logEntries : {},
|
||||
rawLog: pdf.rawLog ? pdf.rawLog : ''
|
||||
logEntries: pdf.logEntries,
|
||||
validationIssues: pdf.validation,
|
||||
errors: clsiErrors,
|
||||
rawLog: pdf.rawLog,
|
||||
compileFailed: pdf.compileFailed
|
||||
}`
|
||||
on-clear-cache="clearCache"
|
||||
on-recompile="recompile"
|
||||
|
@ -269,115 +272,111 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
ng-if="settings.pdfViewer == 'native'"
|
||||
)
|
||||
|
||||
.pdf-validation-problems(ng-switch-when="validation-problems")
|
||||
if !showNewLogsUI
|
||||
.pdf-validation-problems(ng-switch-when="validation-problems")
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.validation.duplicatePaths")
|
||||
strong #{translate("latex_error")}
|
||||
span #{translate("duplicate_paths_found")}
|
||||
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.validation.sizeCheck")
|
||||
strong #{translate("project_too_large")}
|
||||
div #{translate("project_too_large_please_reduce")}
|
||||
div
|
||||
li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.validation.conflictedPaths")
|
||||
div
|
||||
strong #{translate("conflicting_paths_found")}
|
||||
div !{translate("following_paths_conflict")}
|
||||
div
|
||||
li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.validation.mainFile")
|
||||
strong #{translate("main_file_not_found")}
|
||||
span #{translate("please_set_main_file")}
|
||||
|
||||
.pdf-errors(ng-switch-when="errors")
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.error")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("somthing_went_wrong_compiling")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.renderingError")
|
||||
strong #{translate("pdf_rendering_error")}
|
||||
span #{translate("something_went_wrong_rendering_pdf")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.clsiMaintenance")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("clsi_maintenance")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.clsiUnavailable")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("clsi_unavailable")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.tooRecentlyCompiled")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("too_recently_compiled")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.compileTerminated")
|
||||
strong #{translate("terminated")}.
|
||||
span #{translate("compile_terminated_by_user")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.rateLimited")
|
||||
strong #{translate("pdf_compile_rate_limit_hit")}
|
||||
span #{translate("project_flagged_too_many_compiles")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.compileInProgress")
|
||||
strong #{translate("pdf_compile_in_progress_error")}.
|
||||
span #{translate("pdf_compile_try_again")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.timedout")
|
||||
p
|
||||
strong #{translate("timedout")}.
|
||||
span #{translate("proj_timed_out_reason")}
|
||||
p
|
||||
a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank")
|
||||
| #{translate("learn_how_to_make_documents_compile_quickly")}
|
||||
|
||||
if settings.enableSubscriptions
|
||||
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
|
||||
p(ng-if="project.owner._id == user.id")
|
||||
strong #{translate("upgrade_for_longer_compiles")}
|
||||
p(ng-if="project.owner._id != user.id")
|
||||
strong #{translate("ask_proj_owner_to_upgrade_for_longer_compiles")}
|
||||
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
|
||||
p #{translate("plus_upgraded_accounts_receive")}:
|
||||
.alert.alert-danger(ng-show="pdf.validation.sizeCheck")
|
||||
strong #{translate("project_too_large")}
|
||||
div #{translate("project_too_large_please_reduce")}
|
||||
div
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id")
|
||||
a.btn.btn-success.row-spaced-small(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('compile-timeout')"
|
||||
) #{translate("start_free_trial")}
|
||||
li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.autoCompileDisabled")
|
||||
p
|
||||
strong #{translate("autocompile_disabled")}.
|
||||
span #{translate("autocompile_disabled_reason")}
|
||||
.alert.alert-danger(ng-show="pdf.validation.conflictedPaths")
|
||||
div
|
||||
strong #{translate("conflicting_paths_found")}
|
||||
div !{translate("following_paths_conflict")}
|
||||
div
|
||||
li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.projectTooLarge")
|
||||
strong #{translate("project_too_large")}
|
||||
span #{translate("project_too_large_please_reduce")}
|
||||
.alert.alert-danger(ng-show="pdf.validation.mainFile")
|
||||
strong #{translate("main_file_not_found")}
|
||||
span #{translate("please_set_main_file")}
|
||||
|
||||
.pdf-errors(ng-switch-when="errors")
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.error")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("somthing_went_wrong_compiling")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.renderingError")
|
||||
strong #{translate("pdf_rendering_error")}
|
||||
span #{translate("something_went_wrong_rendering_pdf")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.clsiMaintenance")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("clsi_maintenance")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.clsiUnavailable")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("clsi_unavailable")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.tooRecentlyCompiled")
|
||||
strong #{translate("server_error")}
|
||||
span #{translate("too_recently_compiled")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.compileTerminated")
|
||||
strong #{translate("terminated")}.
|
||||
span #{translate("compile_terminated_by_user")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.rateLimited")
|
||||
strong #{translate("pdf_compile_rate_limit_hit")}
|
||||
span #{translate("project_flagged_too_many_compiles")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.compileInProgress")
|
||||
strong #{translate("pdf_compile_in_progress_error")}.
|
||||
span #{translate("pdf_compile_try_again")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.timedout")
|
||||
p
|
||||
strong #{translate("timedout")}.
|
||||
span #{translate("proj_timed_out_reason")}
|
||||
p
|
||||
a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank")
|
||||
| #{translate("learn_how_to_make_documents_compile_quickly")}
|
||||
|
||||
if settings.enableSubscriptions
|
||||
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
|
||||
p(ng-if="project.owner._id == user.id")
|
||||
strong #{translate("upgrade_for_longer_compiles")}
|
||||
p(ng-if="project.owner._id != user.id")
|
||||
strong #{translate("ask_proj_owner_to_upgrade_for_longer_compiles")}
|
||||
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
|
||||
p #{translate("plus_upgraded_accounts_receive")}:
|
||||
div
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id")
|
||||
a.btn.btn-success.row-spaced-small(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('compile-timeout')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.autoCompileDisabled")
|
||||
p
|
||||
strong #{translate("autocompile_disabled")}.
|
||||
span #{translate("autocompile_disabled_reason")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.projectTooLarge")
|
||||
strong #{translate("project_too_large")}
|
||||
span #{translate("project_too_large_please_reduce")}
|
||||
|
||||
|
||||
script(type='text/ng-template', id='clearCacheModalTemplate')
|
||||
|
@ -398,3 +397,6 @@ script(type='text/ng-template', id='clearCacheModalTemplate')
|
|||
)
|
||||
span(ng-show="!state.inflight") #{translate("clear_cache")}
|
||||
span(ng-show="state.inflight") #{translate("clearing")}…
|
||||
|
||||
script.
|
||||
window.showNewLogsUI = #{showNewLogsUI}
|
||||
|
|
|
@ -1,22 +1,35 @@
|
|||
[
|
||||
"auto_compile",
|
||||
"autocompile_disabled_reason",
|
||||
"autocompile_disabled",
|
||||
"clsi_maintenance",
|
||||
"clsi_unavailable",
|
||||
"collapse",
|
||||
"compile_error_description",
|
||||
"compile_error_entry_description",
|
||||
"compile_mode",
|
||||
"compile_terminated_by_user",
|
||||
"compiling",
|
||||
"conflicting_paths_found",
|
||||
"dismiss_error_popup",
|
||||
"download_file",
|
||||
"download_pdf",
|
||||
"duplicate_paths_found",
|
||||
"expand",
|
||||
"fast",
|
||||
"file_outline",
|
||||
"find_out_more_about_the_file_outline",
|
||||
"first_error_popup_label",
|
||||
"following_paths_conflict",
|
||||
"go_to_error_location",
|
||||
"hide_outline",
|
||||
"ignore_validation_errors",
|
||||
"latex_error",
|
||||
"learn_how_to_make_documents_compile_quickly",
|
||||
"loading",
|
||||
"log_entry_description",
|
||||
"log_hint_extra_info",
|
||||
"main_file_not_found",
|
||||
"n_errors_plural",
|
||||
"n_errors",
|
||||
"n_warnings_plural",
|
||||
|
@ -27,18 +40,34 @@
|
|||
"off",
|
||||
"on",
|
||||
"other_output_files",
|
||||
"pdf_compile_in_progress_error",
|
||||
"pdf_compile_rate_limit_hit",
|
||||
"pdf_compile_try_again",
|
||||
"pdf_rendering_error",
|
||||
"please_set_main_file",
|
||||
"proj_timed_out_reason",
|
||||
"project_flagged_too_many_compiles",
|
||||
"project_too_large_please_reduce",
|
||||
"project_too_large",
|
||||
"raw_logs_description",
|
||||
"raw_logs",
|
||||
"recompile_from_scratch",
|
||||
"recompile",
|
||||
"run_syntax_check_now",
|
||||
"run_syntax_check_now",
|
||||
"send_first_message",
|
||||
"server_error",
|
||||
"show_outline",
|
||||
"something_went_wrong_rendering_pdf",
|
||||
"somthing_went_wrong_compiling",
|
||||
"stop_on_validation_error",
|
||||
"terminated",
|
||||
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
|
||||
"timedout",
|
||||
"toggle_compile_options_menu",
|
||||
"toggle_output_files_list",
|
||||
"too_recently_compiled",
|
||||
"validation_issue_description",
|
||||
"validation_issue_entry_description",
|
||||
"view_all_errors",
|
||||
"view_logs",
|
||||
"view_pdf",
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
|
||||
|
||||
function PreviewError({ name }) {
|
||||
const { t } = useTranslation()
|
||||
let errorTitle
|
||||
let errorContent
|
||||
|
||||
if (name === 'error') {
|
||||
errorTitle = t('server_error')
|
||||
errorContent = <>{t('somthing_went_wrong_compiling')}</>
|
||||
} else if (name === 'renderingError') {
|
||||
errorTitle = t('pdf_rendering_error')
|
||||
errorContent = <>{t('something_went_wrong_rendering_pdf')}</>
|
||||
} else if (name === 'clsiMaintenance') {
|
||||
errorTitle = t('server_error')
|
||||
errorContent = <>{t('clsi_maintenance')}</>
|
||||
} else if (name === 'clsiUnavailable') {
|
||||
errorTitle = t('server_error')
|
||||
errorContent = <>{t('clsi_unavailable')}</>
|
||||
} else if (name === 'tooRecentlyCompiled') {
|
||||
errorTitle = t('server_error')
|
||||
errorContent = <>{t('too_recently_compiled')}</>
|
||||
} else if (name === 'compileTerminated') {
|
||||
errorTitle = t('terminated')
|
||||
errorContent = <>{t('compile_terminated_by_user')}</>
|
||||
} else if (name === 'rateLimited') {
|
||||
errorTitle = t('pdf_compile_rate_limit_hit')
|
||||
errorContent = <>{t('project_flagged_too_many_compiles')}</>
|
||||
} else if (name === 'compileInProgress') {
|
||||
errorTitle = t('pdf_compile_in_progress_error')
|
||||
errorContent = <>{t('pdf_compile_try_again')}</>
|
||||
} else if (name === 'timedout') {
|
||||
errorTitle = t('timedout')
|
||||
errorContent = (
|
||||
<>
|
||||
{t('proj_timed_out_reason')}
|
||||
<div>
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F"
|
||||
target="_blank"
|
||||
>
|
||||
{t('learn_how_to_make_documents_compile_quickly')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else if (name === 'autoCompileDisabled') {
|
||||
errorTitle = t('autocompile_disabled')
|
||||
errorContent = <>{t('autocompile_disabled_reason')}</>
|
||||
}
|
||||
|
||||
return errorTitle ? (
|
||||
<PreviewLogsPaneEntry
|
||||
headerTitle={errorTitle}
|
||||
formattedContent={errorContent}
|
||||
entryAriaLabel={t('compile_error_entry_description')}
|
||||
level="error"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
PreviewError.propTypes = {
|
||||
name: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default PreviewError
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import PreviewLogEntry from './preview-log-entry'
|
||||
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
|
||||
|
||||
function PreviewFirstErrorPopUp({
|
||||
logEntry,
|
||||
|
@ -23,8 +23,12 @@ function PreviewFirstErrorPopUp({
|
|||
role="alertdialog"
|
||||
aria-label={t('first_error_popup_label')}
|
||||
>
|
||||
<PreviewLogEntry
|
||||
{...logEntry}
|
||||
<PreviewLogsPaneEntry
|
||||
headerTitle={logEntry.message}
|
||||
rawContent={logEntry.content}
|
||||
formattedContent={logEntry.humanReadableHintComponent}
|
||||
extraInfoURL={logEntry.extraInfoURL}
|
||||
level={logEntry.level}
|
||||
showLineAndNoLink={false}
|
||||
showCloseButton
|
||||
onClose={onClose}
|
||||
|
|
|
@ -5,46 +5,38 @@ import { useTranslation } from 'react-i18next'
|
|||
import useExpandCollapse from '../../../shared/hooks/use-expand-collapse'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
function PreviewLogEntry({
|
||||
file,
|
||||
line,
|
||||
message,
|
||||
content,
|
||||
column,
|
||||
humanReadableHintComponent,
|
||||
function PreviewLogsPaneEntry({
|
||||
headerTitle,
|
||||
rawContent,
|
||||
formattedContent,
|
||||
extraInfoURL,
|
||||
level,
|
||||
showLineAndNoLink = true,
|
||||
sourceLocation,
|
||||
showSourceLocationLink = true,
|
||||
showCloseButton = false,
|
||||
onLogEntryLocationClick,
|
||||
entryAriaLabel = null,
|
||||
onSourceLocationClick,
|
||||
onClose
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
function handleLogEntryLinkClick() {
|
||||
onLogEntryLocationClick({ file, line, column })
|
||||
onSourceLocationClick(sourceLocation)
|
||||
}
|
||||
const logEntryDescription =
|
||||
level === 'raw'
|
||||
? t('raw_logs_description')
|
||||
: t('log_entry_description', {
|
||||
level: level
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="log-entry" aria-label={logEntryDescription}>
|
||||
<div className="log-entry" aria-label={entryAriaLabel}>
|
||||
<PreviewLogEntryHeader
|
||||
level={level}
|
||||
file={file}
|
||||
line={line}
|
||||
message={message}
|
||||
showLineAndNoLink={showLineAndNoLink}
|
||||
onLogEntryLocationClick={handleLogEntryLinkClick}
|
||||
sourceLocation={sourceLocation}
|
||||
headerTitle={headerTitle}
|
||||
showSourceLocationLink={showSourceLocationLink}
|
||||
onSourceLocationClick={handleLogEntryLinkClick}
|
||||
showCloseButton={showCloseButton}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{content ? (
|
||||
{rawContent || formattedContent ? (
|
||||
<PreviewLogEntryContent
|
||||
content={content}
|
||||
humanReadableHintComponent={humanReadableHintComponent}
|
||||
rawContent={rawContent}
|
||||
formattedContent={formattedContent}
|
||||
extraInfoURL={extraInfoURL}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -53,16 +45,17 @@ function PreviewLogEntry({
|
|||
}
|
||||
|
||||
function PreviewLogEntryHeader({
|
||||
sourceLocation,
|
||||
level,
|
||||
file,
|
||||
line,
|
||||
message,
|
||||
showLineAndNoLink = true,
|
||||
headerTitle,
|
||||
showSourceLocationLink = true,
|
||||
showCloseButton = false,
|
||||
onLogEntryLocationClick,
|
||||
onSourceLocationClick,
|
||||
onClose
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const file = sourceLocation ? sourceLocation.file : null
|
||||
const line = sourceLocation ? sourceLocation.line : null
|
||||
const logEntryHeaderClasses = classNames('log-entry-header', {
|
||||
'log-entry-header-error': level === 'error',
|
||||
'log-entry-header-warning': level === 'warning',
|
||||
|
@ -75,13 +68,13 @@ function PreviewLogEntryHeader({
|
|||
|
||||
return (
|
||||
<header className={logEntryHeaderClasses}>
|
||||
<h3 className="log-entry-header-title">{message}</h3>
|
||||
{showLineAndNoLink && file ? (
|
||||
<h3 className="log-entry-header-title">{headerTitle}</h3>
|
||||
{showSourceLocationLink && file ? (
|
||||
<button
|
||||
className="btn-inline-link log-entry-header-link"
|
||||
type="button"
|
||||
title={headerLogLocationTitle}
|
||||
onClick={onLogEntryLocationClick}
|
||||
onClick={onSourceLocationClick}
|
||||
>
|
||||
<Icon type="chain" />
|
||||
|
||||
|
@ -89,7 +82,7 @@ function PreviewLogEntryHeader({
|
|||
{line ? <span>, {line}</span> : null}
|
||||
</button>
|
||||
) : null}
|
||||
{showCloseButton && file ? (
|
||||
{showCloseButton ? (
|
||||
<button
|
||||
className="btn-inline-link log-entry-header-link"
|
||||
type="button"
|
||||
|
@ -104,8 +97,8 @@ function PreviewLogEntryHeader({
|
|||
}
|
||||
|
||||
function PreviewLogEntryContent({
|
||||
content,
|
||||
humanReadableHintComponent,
|
||||
rawContent,
|
||||
formattedContent,
|
||||
extraInfoURL
|
||||
}) {
|
||||
const { isExpanded, expandableProps, toggleProps } = useExpandCollapse({
|
||||
|
@ -127,38 +120,34 @@ function PreviewLogEntryContent({
|
|||
|
||||
return (
|
||||
<div className="log-entry-content">
|
||||
<div {...expandableProps}>
|
||||
<pre className={logContentClasses}>{content.trim()}</pre>
|
||||
<div className={buttonContainerClasses}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-default log-entry-btn-expand-collapse"
|
||||
{...toggleProps}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<Icon type="angle-up" /> {t('collapse')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon type="angle-down" /> {t('expand')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{humanReadableHintComponent ? (
|
||||
<div className="log-entry-human-readable-hint">
|
||||
{humanReadableHintComponent}
|
||||
{rawContent ? (
|
||||
<div {...expandableProps}>
|
||||
<pre className={logContentClasses}>{rawContent.trim()}</pre>
|
||||
<div className={buttonContainerClasses}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-default log-entry-btn-expand-collapse"
|
||||
{...toggleProps}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<Icon type="angle-up" /> {t('collapse')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon type="angle-down" /> {t('expand')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{formattedContent ? (
|
||||
<div className="log-entry-formatted-content">{formattedContent}</div>
|
||||
) : null}
|
||||
{extraInfoURL ? (
|
||||
<div className="log-entry-human-readable-hint-link">
|
||||
<a
|
||||
href={extraInfoURL}
|
||||
target="_blank"
|
||||
className="log-entry-human-readable-hint-link"
|
||||
>
|
||||
<div className="log-entry-content-link">
|
||||
<a href={extraInfoURL} target="_blank">
|
||||
{t('log_hint_extra_info')}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -168,41 +157,40 @@ function PreviewLogEntryContent({
|
|||
}
|
||||
|
||||
PreviewLogEntryHeader.propTypes = {
|
||||
sourceLocation: PropTypes.shape({
|
||||
file: PropTypes.string,
|
||||
// `line should be either a number or null (i.e. not required), but currently sometimes we get
|
||||
// an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
|
||||
// to PropTypes.number (not required) once we fix that.
|
||||
line: PropTypes.any,
|
||||
column: PropTypes.any
|
||||
}),
|
||||
level: PropTypes.string.isRequired,
|
||||
file: PropTypes.string,
|
||||
line: PropTypes.any,
|
||||
message: PropTypes.string,
|
||||
showLineAndNoLink: PropTypes.bool,
|
||||
headerTitle: PropTypes.string,
|
||||
showSourceLocationLink: PropTypes.bool,
|
||||
showCloseButton: PropTypes.bool,
|
||||
onLogEntryLocationClick: PropTypes.func,
|
||||
onSourceLocationClick: PropTypes.func,
|
||||
onClose: PropTypes.func
|
||||
}
|
||||
|
||||
PreviewLogEntryContent.propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
humanReadableHintComponent: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.element
|
||||
]),
|
||||
rawContent: PropTypes.string,
|
||||
formattedContent: PropTypes.node,
|
||||
extraInfoURL: PropTypes.string
|
||||
}
|
||||
|
||||
PreviewLogEntry.propTypes = {
|
||||
file: PropTypes.string,
|
||||
// `line should be either a number or null (i.e. not required), but currently sometimes we get
|
||||
// an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
|
||||
// to PropTypes.number (not required) once we fix that.
|
||||
line: PropTypes.any,
|
||||
column: PropTypes.any,
|
||||
message: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
humanReadableHintComponent: PropTypes.node,
|
||||
PreviewLogsPaneEntry.propTypes = {
|
||||
sourceLocation: PreviewLogEntryHeader.propTypes.sourceLocation,
|
||||
headerTitle: PropTypes.string,
|
||||
rawContent: PropTypes.string,
|
||||
formattedContent: PropTypes.node,
|
||||
extraInfoURL: PropTypes.string,
|
||||
level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw']).isRequired,
|
||||
showLineAndNoLink: PropTypes.bool,
|
||||
showSourceLocationLink: PropTypes.bool,
|
||||
showCloseButton: PropTypes.bool,
|
||||
onLogEntryLocationClick: PropTypes.func,
|
||||
entryAriaLabel: PropTypes.string,
|
||||
onSourceLocationClick: PropTypes.func,
|
||||
onClose: PropTypes.func
|
||||
}
|
||||
|
||||
export default PreviewLogEntry
|
||||
export default PreviewLogsPaneEntry
|
|
@ -1,26 +1,68 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewLogEntry from './preview-log-entry'
|
||||
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
|
||||
import PreviewValidationIssue from './preview-validation-issue'
|
||||
import PreviewError from './preview-error'
|
||||
|
||||
function PreviewLogsPane({ logEntries, rawLog, onLogEntryLocationClick }) {
|
||||
function PreviewLogsPane({
|
||||
logEntries = [],
|
||||
rawLog = '',
|
||||
validationIssues = {},
|
||||
errors = {},
|
||||
onLogEntryLocationClick
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const errorsUI = Object.keys(errors).map((name, index) => (
|
||||
<PreviewError key={index} name={name} />
|
||||
))
|
||||
|
||||
const validationIssuesUI = Object.keys(validationIssues).map(
|
||||
(name, index) => (
|
||||
<PreviewValidationIssue
|
||||
key={index}
|
||||
name={name}
|
||||
details={validationIssues[name]}
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
const logEntriesUI = logEntries.map((logEntry, idx) => (
|
||||
<PreviewLogsPaneEntry
|
||||
key={idx}
|
||||
headerTitle={logEntry.message}
|
||||
rawContent={logEntry.content}
|
||||
formattedContent={logEntry.humanReadableHintComponent}
|
||||
extraInfoURL={logEntry.extraInfoURL}
|
||||
level={logEntry.level}
|
||||
entryAriaLabel={t('log_entry_description', {
|
||||
level: logEntry.level
|
||||
})}
|
||||
sourceLocation={{
|
||||
file: logEntry.file,
|
||||
line: logEntry.line,
|
||||
column: logEntry.column
|
||||
}}
|
||||
onSourceLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
))
|
||||
|
||||
const rawLogUI = (
|
||||
<PreviewLogsPaneEntry
|
||||
headerTitle={t('raw_logs')}
|
||||
rawContent={rawLog}
|
||||
entryAriaLabel={t('raw_logs_description')}
|
||||
level="raw"
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="logs-pane">
|
||||
{logEntries && logEntries.length > 0 ? (
|
||||
logEntries.map((logEntry, idx) => (
|
||||
<PreviewLogEntry
|
||||
key={idx}
|
||||
{...logEntry}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div>No logs</div>
|
||||
)}
|
||||
|
||||
<PreviewLogEntry content={rawLog} level="raw" message={t('raw_logs')} />
|
||||
{errors ? errorsUI : null}
|
||||
{validationIssues ? validationIssuesUI : null}
|
||||
{logEntries ? logEntriesUI : null}
|
||||
{rawLog && rawLog !== '' ? rawLogUI : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -28,7 +70,9 @@ function PreviewLogsPane({ logEntries, rawLog, onLogEntryLocationClick }) {
|
|||
PreviewLogsPane.propTypes = {
|
||||
logEntries: PropTypes.array,
|
||||
rawLog: PropTypes.string,
|
||||
onLogEntryLocationClick: PropTypes.func.isRequired
|
||||
onLogEntryLocationClick: PropTypes.func.isRequired,
|
||||
validationIssues: PropTypes.object,
|
||||
errors: PropTypes.object
|
||||
}
|
||||
|
||||
export default PreviewLogsPane
|
||||
|
|
|
@ -8,6 +8,7 @@ import Icon from '../../../shared/components/icon'
|
|||
function PreviewLogsToggleButton({
|
||||
onToggle,
|
||||
showLogs,
|
||||
compileFailed = false,
|
||||
logsState: { nErrors, nWarnings },
|
||||
showText
|
||||
}) {
|
||||
|
@ -41,6 +42,7 @@ function PreviewLogsToggleButton({
|
|||
<button
|
||||
id="logs-toggle"
|
||||
type="button"
|
||||
disabled={compileFailed}
|
||||
className={toggleButtonClasses}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
|
@ -136,7 +138,8 @@ PreviewLogsToggleButton.propTypes = {
|
|||
nLogEntries: PropTypes.number.isRequired
|
||||
}),
|
||||
showLogs: PropTypes.bool.isRequired,
|
||||
showText: PropTypes.bool.isRequired
|
||||
showText: PropTypes.bool.isRequired,
|
||||
compileFailed: PropTypes.bool
|
||||
}
|
||||
|
||||
LogsCompilationResultIndicator.propTypes = {
|
||||
|
|
|
@ -53,6 +53,18 @@ function PreviewPane({
|
|||
? compilerState.logEntries.all.length
|
||||
: 0
|
||||
|
||||
const hasCLSIErrors =
|
||||
compilerState.errors &&
|
||||
Object.keys(compilerState.errors).length > 0 &&
|
||||
compilerState.compileFailed &&
|
||||
!compilerState.isCompiling
|
||||
|
||||
const hasValidationIssues =
|
||||
compilerState.validationIssues &&
|
||||
Object.keys(compilerState.validationIssues).length > 0 &&
|
||||
compilerState.compileFailed &&
|
||||
!compilerState.isCompiling
|
||||
|
||||
const showFirstErrorPopUp =
|
||||
nErrors > 0 &&
|
||||
!seenLogsForCurrentCompile &&
|
||||
|
@ -79,6 +91,12 @@ function PreviewPane({
|
|||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
/>
|
||||
<span aria-live="polite" className="sr-only">
|
||||
{hasCLSIErrors ? t('compile_error_description') : ''}
|
||||
</span>
|
||||
<span aria-live="polite" className="sr-only">
|
||||
{hasValidationIssues ? t('validation_issue_description') : ''}
|
||||
</span>
|
||||
<span aria-live="polite" className="sr-only">
|
||||
{nErrors && !compilerState.isCompiling
|
||||
? t('n_errors', { count: nErrors })
|
||||
|
@ -101,6 +119,8 @@ function PreviewPane({
|
|||
<PreviewLogsPane
|
||||
logEntries={compilerState.logEntries.all}
|
||||
rawLog={compilerState.rawLog}
|
||||
validationIssues={compilerState.validationIssues}
|
||||
errors={compilerState.errors}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -115,8 +135,11 @@ PreviewPane.propTypes = {
|
|||
isDraftModeOn: PropTypes.bool.isRequired,
|
||||
isSyntaxCheckOn: PropTypes.bool.isRequired,
|
||||
lastCompileTimestamp: PropTypes.number,
|
||||
logEntries: PropTypes.object.isRequired,
|
||||
rawLog: PropTypes.string
|
||||
logEntries: PropTypes.object,
|
||||
validationIssues: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
rawLog: PropTypes.string,
|
||||
compileFailed: PropTypes.bool
|
||||
}),
|
||||
onClearCache: PropTypes.func.isRequired,
|
||||
onLogEntryLocationClick: PropTypes.func.isRequired,
|
||||
|
|
|
@ -213,6 +213,7 @@ function PreviewToolbar({
|
|||
<PreviewLogsToggleButton
|
||||
logsState={logsState}
|
||||
showLogs={showLogs}
|
||||
compileFailed={compilerState.compileFailed}
|
||||
onToggle={onToggleLogs}
|
||||
showText={showToggleText}
|
||||
/>
|
||||
|
@ -227,6 +228,7 @@ PreviewToolbar.propTypes = {
|
|||
isCompiling: PropTypes.bool.isRequired,
|
||||
isDraftModeOn: PropTypes.bool.isRequired,
|
||||
isSyntaxCheckOn: PropTypes.bool.isRequired,
|
||||
compileFailed: PropTypes.bool,
|
||||
logEntries: PropTypes.object.isRequired
|
||||
}),
|
||||
logsState: PropTypes.shape({
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
|
||||
|
||||
function PreviewValidationIssue({ name, details }) {
|
||||
const { t } = useTranslation()
|
||||
let validationTitle
|
||||
let validationContent
|
||||
|
||||
if (name === 'sizeCheck') {
|
||||
validationTitle = t('project_too_large')
|
||||
validationContent = (
|
||||
<>
|
||||
<div>{t('project_too_large_please_reduce')}</div>
|
||||
<ul className="list-no-margin-bottom">
|
||||
{details.resources.map((resource, index) => (
|
||||
<li key={index}>
|
||||
{resource.path} — {resource.kbSize}
|
||||
kb
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
} else if (name === 'conflictedPaths') {
|
||||
validationTitle = t('conflicting_paths_found')
|
||||
validationContent = (
|
||||
<>
|
||||
<div>{t('following_paths_conflict')}</div>
|
||||
<ul className="list-no-margin-bottom">
|
||||
{details.map((detail, index) => (
|
||||
<li key={index}>/{detail.path}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
} else if (name === 'mainFile') {
|
||||
validationTitle = t('main_file_not_found')
|
||||
validationContent = <>{t('please_set_main_file')}</>
|
||||
}
|
||||
|
||||
return validationTitle ? (
|
||||
<PreviewLogsPaneEntry
|
||||
headerTitle={validationTitle}
|
||||
formattedContent={validationContent}
|
||||
entryAriaLabel={t('validation_issue_entry_description')}
|
||||
level="error"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
PreviewValidationIssue.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
details: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
PropTypes.bool
|
||||
])
|
||||
}
|
||||
|
||||
export default PreviewValidationIssue
|
|
@ -28,6 +28,7 @@ export default (PdfManager = class PdfManager {
|
|||
logEntries: {},
|
||||
logEntryAnnotations: {},
|
||||
rawLog: '',
|
||||
validation: {},
|
||||
view: null, // 'pdf' 'logs'
|
||||
showRawLog: false,
|
||||
highlights: [],
|
||||
|
|
|
@ -338,6 +338,9 @@ App.controller('PdfController', function(
|
|||
$scope.pdf.failedCheck = false
|
||||
$scope.pdf.compileInProgress = false
|
||||
$scope.pdf.autoCompileDisabled = false
|
||||
if (window.showNewLogsUI) {
|
||||
$scope.clsiErrors = {}
|
||||
}
|
||||
|
||||
// make a cache to look up files by name
|
||||
const fileByPath = {}
|
||||
|
@ -362,6 +365,7 @@ App.controller('PdfController', function(
|
|||
$scope.pdf.view = 'pdf'
|
||||
$scope.shouldShowLogs = false
|
||||
$scope.pdf.lastCompileTimestamp = Date.now()
|
||||
$scope.pdf.validation = {}
|
||||
|
||||
// define the base url. if the pdf file has a build number, pass it to the clsi in the url
|
||||
if (fileByPath['output.pdf'] && fileByPath['output.pdf'].url) {
|
||||
|
@ -479,6 +483,33 @@ App.controller('PdfController', function(
|
|||
$scope.pdf.error = true
|
||||
}
|
||||
|
||||
if (window.showNewLogsUI) {
|
||||
$scope.pdf.compileFailed = false
|
||||
// `$scope.clsiErrors` stores the error states nested within `$scope.pdf`
|
||||
// for use with React's <PreviewPane errors={$scope.clsiErrors}/>
|
||||
$scope.clsiErrors = Object.assign(
|
||||
{},
|
||||
$scope.pdf.error ? { error: true } : null,
|
||||
$scope.pdf.renderingError ? { renderingError: true } : null,
|
||||
$scope.pdf.clsiMaintenance ? { clsiMaintenance: true } : null,
|
||||
$scope.pdf.clsiUnavailable ? { clsiUnavailable: true } : null,
|
||||
$scope.pdf.tooRecentlyCompiled ? { tooRecentlyCompiled: true } : null,
|
||||
$scope.pdf.compileTerminated ? { compileTerminated: true } : null,
|
||||
$scope.pdf.rateLimited ? { rateLimited: true } : null,
|
||||
$scope.pdf.compileInProgress ? { compileInProgress: true } : null,
|
||||
$scope.pdf.timedout ? { timedout: true } : null,
|
||||
$scope.pdf.autoCompileDisabled ? { autoCompileDisabled: true } : null
|
||||
)
|
||||
|
||||
if (
|
||||
$scope.pdf.view === 'errors' ||
|
||||
$scope.pdf.view === 'validation-problems'
|
||||
) {
|
||||
$scope.shouldShowLogs = true
|
||||
$scope.pdf.compileFailed = true
|
||||
}
|
||||
}
|
||||
|
||||
const IGNORE_FILES = ['output.fls', 'output.fdb_latexmk']
|
||||
$scope.pdf.outputFiles = []
|
||||
|
||||
|
@ -765,6 +796,11 @@ App.controller('PdfController', function(
|
|||
$scope.pdf.renderingError = false
|
||||
$scope.pdf.error = true
|
||||
$scope.pdf.view = 'errors'
|
||||
if (window.showNewLogsUI) {
|
||||
$scope.clsiErrors = { error: true }
|
||||
$scope.shouldShowLogs = true
|
||||
$scope.pdf.compileFailed = true
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
$scope.lastFinishedCompileAt = Date.now()
|
||||
|
|
|
@ -1,69 +1,77 @@
|
|||
import React from 'react'
|
||||
import PreviewLogEntry from '../js/features/preview/components/preview-log-entry.js'
|
||||
import PreviewLogsPaneEntry from '../js/features/preview/components/preview-logs-pane-entry.js'
|
||||
|
||||
export const ErrorWithCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
export const ErrorWithCompilerOutput = args => (
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
ErrorWithCompilerOutput.args = {
|
||||
level: 'error'
|
||||
}
|
||||
|
||||
export const ErrorWithCompilerOutputAndHumanReadableHint = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
ErrorWithCompilerOutputAndHumanReadableHint.args = {
|
||||
level: 'error',
|
||||
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
|
||||
formattedContent: <SampleHumanReadableHintComponent />,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
|
||||
}
|
||||
|
||||
export const ErrorWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
export const ErrorWithoutCompilerOutput = args => (
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
ErrorWithoutCompilerOutput.args = {
|
||||
level: 'error',
|
||||
content: null
|
||||
rawContent: null
|
||||
}
|
||||
|
||||
export const WarningWithCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
export const WarningWithCompilerOutput = args => (
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
WarningWithCompilerOutput.args = {
|
||||
level: 'warning'
|
||||
}
|
||||
|
||||
export const WarningWithCompilerOutputAndHumanReadableHint = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
WarningWithCompilerOutputAndHumanReadableHint.args = {
|
||||
level: 'warning',
|
||||
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
|
||||
formattedContent: <SampleHumanReadableHintComponent />,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
|
||||
}
|
||||
|
||||
export const WarningWithoutCompilerOutput = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
WarningWithoutCompilerOutput.args = {
|
||||
level: 'warning',
|
||||
content: null
|
||||
rawContent: null
|
||||
}
|
||||
|
||||
export const InfoWithCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
export const InfoWithCompilerOutput = args => <PreviewLogsPaneEntry {...args} />
|
||||
InfoWithCompilerOutput.args = {
|
||||
level: 'typesetting'
|
||||
}
|
||||
|
||||
export const InfoWithCompilerOutputAndHumanReadableHint = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
InfoWithCompilerOutputAndHumanReadableHint.args = {
|
||||
level: 'typesetting',
|
||||
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
|
||||
formattedContent: <SampleHumanReadableHintComponent />,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
|
||||
}
|
||||
|
||||
export const InfoWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
export const InfoWithoutCompilerOutput = args => (
|
||||
<PreviewLogsPaneEntry {...args} />
|
||||
)
|
||||
InfoWithoutCompilerOutput.args = {
|
||||
level: 'typesetting',
|
||||
content: null
|
||||
rawContent: null
|
||||
}
|
||||
|
||||
function SampleHumanReadableHintComponent() {
|
||||
|
@ -84,14 +92,16 @@ function SampleHumanReadableHintComponent() {
|
|||
}
|
||||
|
||||
export default {
|
||||
title: 'PreviewLogEntry',
|
||||
component: PreviewLogEntry,
|
||||
title: 'PreviewLogsPaneEntry',
|
||||
component: PreviewLogsPaneEntry,
|
||||
args: {
|
||||
file: 'foo/bar.tex',
|
||||
line: 10,
|
||||
column: 20,
|
||||
message: 'Lorem ipsum',
|
||||
content: `
|
||||
sourceLocation: {
|
||||
file: 'foo/bar.tex',
|
||||
line: 10,
|
||||
column: 20
|
||||
},
|
||||
headerTitle: 'Lorem ipsum',
|
||||
rawContent: `
|
||||
The LaTeX compiler output
|
||||
* With a lot of details
|
||||
|
|
@ -125,10 +125,13 @@
|
|||
.no-outline-ring-on-click;
|
||||
}
|
||||
|
||||
.log-entry-human-readable-hint,
|
||||
.log-entry-human-readable-hint-link {
|
||||
.log-entry-formatted-content,
|
||||
.log-entry-content-link {
|
||||
font-size: @font-size-small;
|
||||
margin-top: @margin-sm;
|
||||
margin-top: @margin-xs;
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.first-error-popup {
|
||||
|
|
|
@ -227,6 +227,9 @@ ol {
|
|||
}
|
||||
|
||||
// List options
|
||||
.list-no-margin-bottom {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Unstyled keeps list items block level, just removes default browser padding and list-style
|
||||
.list-unstyled {
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
{
|
||||
"compile_error_description": "Your project did not compile because of an error",
|
||||
"validation_issue_description": "Your project did not compile because of a validation issue",
|
||||
"compile_error_entry_description": "An error which prevented your project from compiling",
|
||||
"validation_issue_entry_description": "A validation issue which prevented your project from compiling",
|
||||
"raw_logs_description": "Raw logs from the LaTeX compiler",
|
||||
"raw_logs": "Raw logs",
|
||||
"first_error_popup_label": "Your project has errors. This is the first one.",
|
||||
|
@ -472,7 +476,7 @@
|
|||
"tc_guests": "Guests",
|
||||
"select_all_projects": "Select all",
|
||||
"select_project": "Select",
|
||||
"main_file_not_found": "Unknown main document.",
|
||||
"main_file_not_found": "Unknown main document",
|
||||
"please_set_main_file": "Please choose the main file for this project in the project menu. ",
|
||||
"link_sharing_is_off": "Link sharing is off, only invited users can view this project.",
|
||||
"turn_on_link_sharing": "Turn on link sharing",
|
||||
|
@ -649,7 +653,7 @@
|
|||
"beta_program_benefits": "We're always improving __appName__. By joining our Beta program you can have early access to new features and help us understand your needs better.",
|
||||
"beta_program_opt_in_action": "Opt-In to Beta Program",
|
||||
"conflicting_paths_found": "Conflicting Paths Found",
|
||||
"following_paths_conflict": "The following files & folders conflict with the same path",
|
||||
"following_paths_conflict": "The following files and folders conflict with the same path",
|
||||
"open_a_file_on_the_left": "Open a file on the left",
|
||||
"reference_error_relink_hint": "If this error persists, try re-linking your account here:",
|
||||
"pdf_rendering_error": "PDF Rendering Error",
|
||||
|
|
|
@ -3,50 +3,47 @@ import { expect } from 'chai'
|
|||
import sinon from 'sinon'
|
||||
import { screen, render, fireEvent } from '@testing-library/react'
|
||||
|
||||
import PreviewLogEntry from '../../../../../frontend/js/features/preview/components/preview-log-entry.js'
|
||||
import PreviewLogsPaneEntry from '../../../../../frontend/js/features/preview/components/preview-logs-pane-entry.js'
|
||||
|
||||
describe('<PreviewLogEntry />', function() {
|
||||
describe('<PreviewLogsPaneEntry />', function() {
|
||||
const level = 'error'
|
||||
|
||||
describe('log entry description', function() {
|
||||
for (const level of ['error', 'warning', 'typesetting', 'raw']) {
|
||||
it(`describes the log entry with ${level} information`, function() {
|
||||
render(<PreviewLogEntry level={level} />)
|
||||
const expectedLabel =
|
||||
level === 'raw'
|
||||
? 'Raw logs from the LaTeX compiler'
|
||||
: `Log entry with level: ${level}`
|
||||
screen.getByLabelText(expectedLabel)
|
||||
})
|
||||
}
|
||||
it('renders a configurable aria-label', function() {
|
||||
const sampleAriaLabel = 'lorem ipsum dolor sit amet'
|
||||
render(
|
||||
<PreviewLogsPaneEntry entryAriaLabel={sampleAriaLabel} level={level} />
|
||||
)
|
||||
screen.getByLabelText(sampleAriaLabel)
|
||||
})
|
||||
|
||||
describe('log location link', function() {
|
||||
describe('logs pane source location link', function() {
|
||||
const file = 'foo.tex'
|
||||
const line = 42
|
||||
const column = 21
|
||||
const onLogEntryLocationClick = sinon.stub()
|
||||
const onSourceLocationClick = sinon.stub()
|
||||
|
||||
afterEach(function() {
|
||||
onLogEntryLocationClick.reset()
|
||||
onSourceLocationClick.reset()
|
||||
})
|
||||
|
||||
it('renders both file and line', function() {
|
||||
render(<PreviewLogEntry file={file} line={line} level={level} />)
|
||||
render(
|
||||
<PreviewLogsPaneEntry sourceLocation={{ file, line }} level={level} />
|
||||
)
|
||||
screen.getByRole('button', {
|
||||
name: `Navigate to log position in source code: ${file}, ${line}`
|
||||
})
|
||||
})
|
||||
|
||||
it('renders only file when line information is not available', function() {
|
||||
render(<PreviewLogEntry file={file} level={level} />)
|
||||
render(<PreviewLogsPaneEntry sourceLocation={{ file }} level={level} />)
|
||||
screen.getByRole('button', {
|
||||
name: `Navigate to log position in source code: ${file}`
|
||||
})
|
||||
})
|
||||
|
||||
it('does not render when file information is not available', function() {
|
||||
render(<PreviewLogEntry level={level} />)
|
||||
render(<PreviewLogsPaneEntry level={level} />)
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: `Navigate to log position in source code: `
|
||||
|
@ -56,12 +53,10 @@ describe('<PreviewLogEntry />', function() {
|
|||
|
||||
it('calls the callback with file, line and column on click', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
file={file}
|
||||
line={line}
|
||||
column={column}
|
||||
<PreviewLogsPaneEntry
|
||||
sourceLocation={{ file, line, column }}
|
||||
level={level}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
onSourceLocationClick={onSourceLocationClick}
|
||||
/>
|
||||
)
|
||||
const linkToSourceButton = screen.getByRole('button', {
|
||||
|
@ -69,29 +64,29 @@ describe('<PreviewLogEntry />', function() {
|
|||
})
|
||||
|
||||
fireEvent.click(linkToSourceButton)
|
||||
expect(onLogEntryLocationClick).to.be.calledOnce
|
||||
expect(onLogEntryLocationClick).to.be.calledWith({
|
||||
expect(onSourceLocationClick).to.be.calledOnce
|
||||
expect(onSourceLocationClick).to.be.calledWith({
|
||||
file,
|
||||
line: line,
|
||||
column: column
|
||||
line,
|
||||
column
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('log entry contents', function() {
|
||||
const logContent = 'foo bar latex error stuff baz'
|
||||
describe('logs pane entry raw contents', function() {
|
||||
const rawContent = 'foo bar latex error stuff baz'
|
||||
|
||||
it('renders collapsed contents by default', function() {
|
||||
render(<PreviewLogEntry content={logContent} level={level} />)
|
||||
screen.getByText(logContent)
|
||||
render(<PreviewLogsPaneEntry rawContent={rawContent} level={level} />)
|
||||
screen.getByText(rawContent)
|
||||
screen.getByRole('button', {
|
||||
name: 'Expand'
|
||||
})
|
||||
})
|
||||
|
||||
it('supports expanding contents', function() {
|
||||
render(<PreviewLogEntry content={logContent} level={level} />)
|
||||
screen.getByText(logContent)
|
||||
render(<PreviewLogsPaneEntry rawContent={rawContent} level={level} />)
|
||||
screen.getByText(rawContent)
|
||||
const expandCollapseBtn = screen.getByRole('button', {
|
||||
name: 'Expand'
|
||||
})
|
||||
|
@ -102,34 +97,34 @@ describe('<PreviewLogEntry />', function() {
|
|||
})
|
||||
|
||||
it('should not render at all when there are no log contents', function() {
|
||||
const { container } = render(<PreviewLogEntry level={level} />)
|
||||
const { container } = render(<PreviewLogsPaneEntry level={level} />)
|
||||
expect(container.querySelector('.log-entry-content')).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('human-readable hints', function() {
|
||||
const logContent = 'foo bar latex error stuff baz'
|
||||
const logHintText = 'foo bar baz'
|
||||
const logHint = <>{logHintText}</>
|
||||
describe('formatted content', function() {
|
||||
const rawContent = 'foo bar latex error stuff baz'
|
||||
const formattedContentText = 'foo bar baz'
|
||||
const formattedContent = <>{formattedContentText}</>
|
||||
const infoURL = 'www.overleaf.com/learn/latex'
|
||||
|
||||
it('renders the hint', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
humanReadableHintComponent={logHint}
|
||||
<PreviewLogsPaneEntry
|
||||
rawContent={rawContent}
|
||||
formattedContent={formattedContent}
|
||||
extraInfoURL={infoURL}
|
||||
level={level}
|
||||
/>
|
||||
)
|
||||
screen.getByText(logHintText)
|
||||
screen.getByText(formattedContentText)
|
||||
})
|
||||
|
||||
it('renders the link to learn more', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
humanReadableHintComponent={logHint}
|
||||
<PreviewLogsPaneEntry
|
||||
rawContent={rawContent}
|
||||
formattedContent={formattedContent}
|
||||
extraInfoURL={infoURL}
|
||||
level={level}
|
||||
/>
|
||||
|
@ -139,9 +134,9 @@ describe('<PreviewLogEntry />', function() {
|
|||
|
||||
it('does not render the link when it is not available', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
humanReadableHintComponent={logHint}
|
||||
<PreviewLogsPaneEntry
|
||||
rawContent={rawContent}
|
||||
formattedContent={formattedContent}
|
||||
level={level}
|
||||
/>
|
||||
)
|
|
@ -26,6 +26,12 @@ describe('<PreviewLogsPane />', function() {
|
|||
line: 30,
|
||||
message: "Reference `idontexist' on page 1 undefined on input line 30."
|
||||
}
|
||||
const sampleTypesettingIssue = {
|
||||
file: 'main.tex',
|
||||
level: 'typesetting',
|
||||
line: 12,
|
||||
message: "Reference `idontexist' on page 1 undefined on input line 30."
|
||||
}
|
||||
const sampleRawLog = `
|
||||
This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex 2020.9.10) 6 NOV 2020 15:23
|
||||
entering extended mode
|
||||
|
@ -42,55 +48,138 @@ entering extended mode
|
|||
)`
|
||||
const errors = [sampleError1, sampleError2]
|
||||
const warnings = [sampleWarning]
|
||||
const logEntries = [...errors, ...warnings]
|
||||
const typesetting = [sampleTypesettingIssue]
|
||||
const logEntries = [...errors, ...warnings, ...typesetting]
|
||||
|
||||
const onLogEntryLocationClick = sinon.stub()
|
||||
|
||||
beforeEach(function() {
|
||||
render(
|
||||
<PreviewLogsPane
|
||||
logEntries={logEntries}
|
||||
rawLog={sampleRawLog}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
)
|
||||
})
|
||||
it('renders all log entries with appropriate labels', function() {
|
||||
const errorEntries = screen.getAllByLabelText(`Log entry with level: error`)
|
||||
const warningEntries = screen.getAllByLabelText(
|
||||
`Log entry with level: warning`
|
||||
)
|
||||
expect(errorEntries).to.have.lengthOf(errors.length)
|
||||
expect(warningEntries).to.have.lengthOf(warnings.length)
|
||||
})
|
||||
describe('with logs', function() {
|
||||
beforeEach(function() {
|
||||
render(
|
||||
<PreviewLogsPane
|
||||
logEntries={logEntries}
|
||||
rawLog={sampleRawLog}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
)
|
||||
})
|
||||
it('renders all log entries with appropriate labels', function() {
|
||||
const errorEntries = screen.getAllByLabelText(
|
||||
`Log entry with level: error`
|
||||
)
|
||||
const warningEntries = screen.getAllByLabelText(
|
||||
`Log entry with level: warning`
|
||||
)
|
||||
const typesettingEntries = screen.getAllByLabelText(
|
||||
`Log entry with level: typesetting`
|
||||
)
|
||||
expect(errorEntries).to.have.lengthOf(errors.length)
|
||||
expect(warningEntries).to.have.lengthOf(warnings.length)
|
||||
expect(typesettingEntries).to.have.lengthOf(typesetting.length)
|
||||
})
|
||||
|
||||
it('renders the raw log', function() {
|
||||
screen.getByLabelText('Raw logs from the LaTeX compiler')
|
||||
})
|
||||
it('renders the raw log', function() {
|
||||
screen.getByLabelText('Raw logs from the LaTeX compiler')
|
||||
})
|
||||
|
||||
it('renders a link to location button for every error and warning log entry', function() {
|
||||
logEntries.forEach((entry, index) => {
|
||||
const linkToSourceButton = screen.getByRole('button', {
|
||||
name: `Navigate to log position in source code: ${entry.file}, ${
|
||||
entry.line
|
||||
}`
|
||||
})
|
||||
fireEvent.click(linkToSourceButton)
|
||||
expect(onLogEntryLocationClick).to.have.callCount(index + 1)
|
||||
const call = onLogEntryLocationClick.getCall(index)
|
||||
expect(
|
||||
call.calledWith({
|
||||
file: entry.file,
|
||||
line: entry.line,
|
||||
column: entry.column
|
||||
it('renders a link to location button for every error and warning log entry', function() {
|
||||
logEntries.forEach((entry, index) => {
|
||||
const linkToSourceButton = screen.getByRole('button', {
|
||||
name: `Navigate to log position in source code: ${entry.file}, ${
|
||||
entry.line
|
||||
}`
|
||||
})
|
||||
).to.be.true
|
||||
fireEvent.click(linkToSourceButton)
|
||||
expect(onLogEntryLocationClick).to.have.callCount(index + 1)
|
||||
const call = onLogEntryLocationClick.getCall(index)
|
||||
expect(
|
||||
call.calledWith({
|
||||
file: entry.file,
|
||||
line: entry.line,
|
||||
column: entry.column
|
||||
})
|
||||
).to.be.true
|
||||
})
|
||||
})
|
||||
it(' does not render a link to location button for the raw log entry', function() {
|
||||
const rawLogEntry = screen.getByLabelText(
|
||||
'Raw logs from the LaTeX compiler'
|
||||
)
|
||||
expect(rawLogEntry.querySelector('.log-entry-header-link')).to.not.exist
|
||||
})
|
||||
})
|
||||
it('does not render a link to location button for the raw log entry', function() {
|
||||
const rawLogEntry = screen.getByLabelText(
|
||||
'Raw logs from the LaTeX compiler'
|
||||
)
|
||||
expect(rawLogEntry.querySelector('.log-entry-header-link')).to.not.exist
|
||||
|
||||
describe('with validation issues', function() {
|
||||
const sampleValidationIssues = {
|
||||
sizeCheck: {
|
||||
resources: [
|
||||
{ path: 'foo/bar', kbSize: 76221 },
|
||||
{ path: 'bar/baz', kbSize: 2342 }
|
||||
]
|
||||
},
|
||||
mainFile: true
|
||||
}
|
||||
|
||||
it('renders a validation entry for known issues', function() {
|
||||
render(
|
||||
<PreviewLogsPane
|
||||
validationIssues={sampleValidationIssues}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
)
|
||||
const validationEntries = screen.getAllByLabelText(
|
||||
'A validation issue which prevented your project from compiling'
|
||||
)
|
||||
expect(validationEntries).to.have.lengthOf(
|
||||
Object.keys(sampleValidationIssues).length
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores unknown issues', function() {
|
||||
render(
|
||||
<PreviewLogsPane
|
||||
validationIssues={{ unknownIssue: true }}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
)
|
||||
const validationEntries = screen.queryAllByLabelText(
|
||||
'A validation issue prevented your project from compiling'
|
||||
)
|
||||
expect(validationEntries).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with compilation errors', function() {
|
||||
const sampleErrors = {
|
||||
clsiMaintenance: true,
|
||||
tooRecentlyCompiled: true,
|
||||
compileTerminated: true
|
||||
}
|
||||
|
||||
it('renders an error entry for known errors', function() {
|
||||
render(
|
||||
<PreviewLogsPane
|
||||
errors={sampleErrors}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
)
|
||||
const errorEntries = screen.getAllByLabelText(
|
||||
'An error which prevented your project from compiling'
|
||||
)
|
||||
expect(errorEntries).to.have.lengthOf(Object.keys(sampleErrors).length)
|
||||
})
|
||||
|
||||
it('ignores unknown errors', function() {
|
||||
render(
|
||||
<PreviewLogsPane
|
||||
errors={{ unknownIssue: true }}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
/>
|
||||
)
|
||||
const errorEntries = screen.queryAllByLabelText(
|
||||
'There was an error compiling your project'
|
||||
)
|
||||
expect(errorEntries).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -197,11 +197,70 @@ describe('<PreviewPane />', function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe('accessible description of the compile result', function() {
|
||||
it('renders an accessible description with the errors and warnings count', function() {
|
||||
const errors = [sampleError1, sampleError2]
|
||||
const warnings = [sampleWarning]
|
||||
const propsWithErrorsAndWarnings = getProps(false, {
|
||||
errors,
|
||||
warnings
|
||||
})
|
||||
render(<PreviewPane {...propsWithErrorsAndWarnings} />)
|
||||
|
||||
screen.getByText(`${errors.length} error${errors.length > 1 ? 's' : ''}`)
|
||||
screen.getByText(
|
||||
`${warnings.length} warning${warnings.length > 1 ? 's' : ''}`
|
||||
)
|
||||
})
|
||||
it('renders an accessible description for failed compiles with CLSI errors', function() {
|
||||
const sampleCLSIError = {
|
||||
clsiMaintenance: true
|
||||
}
|
||||
|
||||
const propsWithCLSIError = getProps(
|
||||
false,
|
||||
{},
|
||||
Date.now(),
|
||||
false,
|
||||
true,
|
||||
{},
|
||||
sampleCLSIError
|
||||
)
|
||||
render(<PreviewPane {...propsWithCLSIError} />)
|
||||
|
||||
screen.getByText('Your project did not compile because of an error')
|
||||
})
|
||||
|
||||
it('renders an accessible description for failed compiles with validation issues', function() {
|
||||
const sampleValidationIssue = {
|
||||
clsiMaintenance: true
|
||||
}
|
||||
|
||||
const propsWithValidationIssue = getProps(
|
||||
false,
|
||||
{},
|
||||
Date.now(),
|
||||
false,
|
||||
true,
|
||||
sampleValidationIssue,
|
||||
{}
|
||||
)
|
||||
render(<PreviewPane {...propsWithValidationIssue} />)
|
||||
|
||||
screen.getByText(
|
||||
'Your project did not compile because of a validation issue'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function getProps(
|
||||
isCompiling = false,
|
||||
logEntries = {},
|
||||
lastCompileTimestamp = Date.now(),
|
||||
isShowingLogs = false
|
||||
isShowingLogs = false,
|
||||
compileFailed = false,
|
||||
validationIssues = {},
|
||||
errors = {}
|
||||
) {
|
||||
return {
|
||||
compilerState: {
|
||||
|
@ -210,8 +269,11 @@ describe('<PreviewPane />', function() {
|
|||
isClearingCache: false,
|
||||
isDraftModeOn: false,
|
||||
isSyntaxCheckOn: false,
|
||||
lastCompileTimestamp: lastCompileTimestamp,
|
||||
logEntries: logEntries
|
||||
lastCompileTimestamp,
|
||||
logEntries,
|
||||
compileFailed,
|
||||
validationIssues,
|
||||
errors
|
||||
},
|
||||
onClearCache: () => {},
|
||||
onLogEntryLocationClick: () => {},
|
||||
|
|
|
@ -57,7 +57,7 @@ describe('ClsiFormatChecker', function() {
|
|||
])
|
||||
})
|
||||
|
||||
it('should call _checkForDuplicatePaths and _checkForConflictingPaths', function(done) {
|
||||
it('should call _checkDocsAreUnderSizeLimit and _checkForConflictingPaths', function(done) {
|
||||
this.ClsiFormatChecker._checkForConflictingPaths = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null)
|
||||
|
|
Loading…
Reference in a new issue