mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Add first error popup (#3369)
* Add first error popup * Address PR feedback GitOrigin-RevId: e924b3e6096584de6f363aae70a62328cd3de83d
This commit is contained in:
parent
addaa355d0
commit
619ec15309
16 changed files with 494 additions and 87 deletions
|
@ -7,6 +7,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
isCompiling: pdf.compiling,
|
isCompiling: pdf.compiling,
|
||||||
isDraftModeOn: draft,
|
isDraftModeOn: draft,
|
||||||
isSyntaxCheckOn: stop_on_validation_error,
|
isSyntaxCheckOn: stop_on_validation_error,
|
||||||
|
lastCompileTimestamp: pdf.lastCompileTimestamp,
|
||||||
logEntries: pdf.logEntries ? pdf.logEntries : {}
|
logEntries: pdf.logEntries ? pdf.logEntries : {}
|
||||||
}`
|
}`
|
||||||
on-clear-cache="clearCache"
|
on-clear-cache="clearCache"
|
||||||
|
@ -19,7 +20,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
output-files="pdf.outputFiles"
|
output-files="pdf.outputFiles"
|
||||||
pdf-download-url="pdf.downloadUrl"
|
pdf-download-url="pdf.downloadUrl"
|
||||||
show-logs="shouldShowLogs"
|
show-logs="shouldShowLogs"
|
||||||
on-log-entry-link-click="openInEditor"
|
on-log-entry-location-click="openInEditor"
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")
|
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")
|
||||||
|
|
|
@ -3,12 +3,15 @@
|
||||||
"collapse",
|
"collapse",
|
||||||
"compile_mode",
|
"compile_mode",
|
||||||
"compiling",
|
"compiling",
|
||||||
|
"dismiss_error_popup",
|
||||||
"download_file",
|
"download_file",
|
||||||
"download_pdf",
|
"download_pdf",
|
||||||
"expand",
|
"expand",
|
||||||
"fast",
|
"fast",
|
||||||
"file_outline",
|
"file_outline",
|
||||||
"find_out_more_about_the_file_outline",
|
"find_out_more_about_the_file_outline",
|
||||||
|
"first_error_popup_label",
|
||||||
|
"go_to_error_location",
|
||||||
"hide_outline",
|
"hide_outline",
|
||||||
"ignore_validation_errors",
|
"ignore_validation_errors",
|
||||||
"loading",
|
"loading",
|
||||||
|
@ -34,6 +37,7 @@
|
||||||
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
|
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
|
||||||
"toggle_compile_options_menu",
|
"toggle_compile_options_menu",
|
||||||
"toggle_output_files_list",
|
"toggle_output_files_list",
|
||||||
|
"view_all_errors",
|
||||||
"view_logs",
|
"view_logs",
|
||||||
"view_pdf",
|
"view_pdf",
|
||||||
"view_warnings",
|
"view_warnings",
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
function PreviewFirstErrorPopUp({
|
||||||
|
logEntry,
|
||||||
|
onGoToErrorLocation,
|
||||||
|
onViewLogs,
|
||||||
|
onClose
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
function handleGoToErrorLocation() {
|
||||||
|
const { file, line, column } = logEntry
|
||||||
|
onGoToErrorLocation({ file, line, column })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="first-error-popup"
|
||||||
|
role="alertdialog"
|
||||||
|
aria-label={t('first_error_popup_label')}
|
||||||
|
>
|
||||||
|
<PreviewLogEntry
|
||||||
|
{...logEntry}
|
||||||
|
showLineAndNoLink={false}
|
||||||
|
showCloseButton
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
<div className="first-error-popup-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-info btn-xs first-error-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={handleGoToErrorLocation}
|
||||||
|
>
|
||||||
|
<Icon type="chain" />
|
||||||
|
|
||||||
|
{t('go_to_error_location')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-info btn-xs first-error-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={onViewLogs}
|
||||||
|
>
|
||||||
|
<Icon type="file-text-o" />
|
||||||
|
|
||||||
|
{t('view_all_errors')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PreviewFirstErrorPopUp.propTypes = {
|
||||||
|
logEntry: PropTypes.object.isRequired,
|
||||||
|
onGoToErrorLocation: PropTypes.func.isRequired,
|
||||||
|
onViewLogs: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewFirstErrorPopUp
|
|
@ -14,11 +14,14 @@ function PreviewLogEntry({
|
||||||
humanReadableHintComponent,
|
humanReadableHintComponent,
|
||||||
extraInfoURL,
|
extraInfoURL,
|
||||||
level,
|
level,
|
||||||
onLogEntryLinkClick
|
showLineAndNoLink = true,
|
||||||
|
showCloseButton = false,
|
||||||
|
onLogEntryLocationClick,
|
||||||
|
onClose
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
function handleLogEntryLinkClick() {
|
function handleLogEntryLinkClick() {
|
||||||
onLogEntryLinkClick({ file, line, column })
|
onLogEntryLocationClick({ file, line, column })
|
||||||
}
|
}
|
||||||
const logEntryDescription = t('log_entry_description', {
|
const logEntryDescription = t('log_entry_description', {
|
||||||
level: level
|
level: level
|
||||||
|
@ -30,7 +33,10 @@ function PreviewLogEntry({
|
||||||
file={file}
|
file={file}
|
||||||
line={line}
|
line={line}
|
||||||
message={message}
|
message={message}
|
||||||
onLogEntryLinkClick={handleLogEntryLinkClick}
|
showLineAndNoLink={showLineAndNoLink}
|
||||||
|
onLogEntryLocationClick={handleLogEntryLinkClick}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
{content ? (
|
{content ? (
|
||||||
<PreviewLogEntryContent
|
<PreviewLogEntryContent
|
||||||
|
@ -48,7 +54,10 @@ function PreviewLogEntryHeader({
|
||||||
file,
|
file,
|
||||||
line,
|
line,
|
||||||
message,
|
message,
|
||||||
onLogEntryLinkClick
|
showLineAndNoLink = true,
|
||||||
|
showCloseButton = false,
|
||||||
|
onLogEntryLocationClick,
|
||||||
|
onClose
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const logEntryHeaderClasses = classNames('log-entry-header', {
|
const logEntryHeaderClasses = classNames('log-entry-header', {
|
||||||
|
@ -56,18 +65,19 @@ function PreviewLogEntryHeader({
|
||||||
'log-entry-header-warning': level === 'warning',
|
'log-entry-header-warning': level === 'warning',
|
||||||
'log-entry-header-typesetting': level === 'typesetting'
|
'log-entry-header-typesetting': level === 'typesetting'
|
||||||
})
|
})
|
||||||
const headerLinkBtnTitle = t('navigate_log_source', {
|
const headerLogLocationTitle = t('navigate_log_source', {
|
||||||
location: file + (line ? `, ${line}` : '')
|
location: file + (line ? `, ${line}` : '')
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={logEntryHeaderClasses}>
|
<header className={logEntryHeaderClasses}>
|
||||||
<h3 className="log-entry-header-title">{message}</h3>
|
<h3 className="log-entry-header-title">{message}</h3>
|
||||||
{file ? (
|
{showLineAndNoLink && file ? (
|
||||||
<button
|
<button
|
||||||
className="btn-inline-link log-entry-header-link"
|
className="btn-inline-link log-entry-header-link"
|
||||||
type="button"
|
type="button"
|
||||||
title={headerLinkBtnTitle}
|
title={headerLogLocationTitle}
|
||||||
onClick={onLogEntryLinkClick}
|
onClick={onLogEntryLocationClick}
|
||||||
>
|
>
|
||||||
<Icon type="chain" />
|
<Icon type="chain" />
|
||||||
|
|
||||||
|
@ -75,6 +85,16 @@ function PreviewLogEntryHeader({
|
||||||
{line ? <span>, {line}</span> : null}
|
{line ? <span>, {line}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showCloseButton && file ? (
|
||||||
|
<button
|
||||||
|
className="btn-inline-link log-entry-header-link"
|
||||||
|
type="button"
|
||||||
|
aria-label={t('dismiss_error_popup')}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -148,7 +168,10 @@ PreviewLogEntryHeader.propTypes = {
|
||||||
file: PropTypes.string,
|
file: PropTypes.string,
|
||||||
line: PropTypes.any,
|
line: PropTypes.any,
|
||||||
message: PropTypes.string,
|
message: PropTypes.string,
|
||||||
onLogEntryLinkClick: PropTypes.func.isRequired
|
showLineAndNoLink: PropTypes.bool,
|
||||||
|
showCloseButton: PropTypes.bool,
|
||||||
|
onLogEntryLocationClick: PropTypes.func,
|
||||||
|
onClose: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
PreviewLogEntryContent.propTypes = {
|
PreviewLogEntryContent.propTypes = {
|
||||||
|
@ -172,7 +195,10 @@ PreviewLogEntry.propTypes = {
|
||||||
humanReadableHintComponent: PropTypes.node,
|
humanReadableHintComponent: PropTypes.node,
|
||||||
extraInfoURL: PropTypes.string,
|
extraInfoURL: PropTypes.string,
|
||||||
level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
|
level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
|
||||||
onLogEntryLinkClick: PropTypes.func.isRequired
|
showLineAndNoLink: PropTypes.bool,
|
||||||
|
showCloseButton: PropTypes.bool,
|
||||||
|
onLogEntryLocationClick: PropTypes.func,
|
||||||
|
onClose: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PreviewLogEntry
|
export default PreviewLogEntry
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import PreviewLogEntry from './preview-log-entry'
|
import PreviewLogEntry from './preview-log-entry'
|
||||||
|
|
||||||
function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
|
function PreviewLogsPane({ logEntries, onLogEntryLocationClick }) {
|
||||||
return (
|
return (
|
||||||
<div className="logs-pane">
|
<div className="logs-pane">
|
||||||
{logEntries && logEntries.length > 0 ? (
|
{logEntries && logEntries.length > 0 ? (
|
||||||
|
@ -10,7 +10,7 @@ function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
|
||||||
<PreviewLogEntry
|
<PreviewLogEntry
|
||||||
key={idx}
|
key={idx}
|
||||||
{...logEntry}
|
{...logEntry}
|
||||||
onLogEntryLinkClick={onLogEntryLinkClick}
|
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
@ -22,7 +22,7 @@ function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
|
||||||
|
|
||||||
PreviewLogsPane.propTypes = {
|
PreviewLogsPane.propTypes = {
|
||||||
logEntries: PropTypes.array,
|
logEntries: PropTypes.array,
|
||||||
onLogEntryLinkClick: PropTypes.func.isRequired
|
onLogEntryLocationClick: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PreviewLogsPane
|
export default PreviewLogsPane
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import PreviewToolbar from './preview-toolbar'
|
import PreviewToolbar from './preview-toolbar'
|
||||||
import PreviewLogsPane from './preview-logs-pane'
|
import PreviewLogsPane from './preview-logs-pane'
|
||||||
|
import PreviewFirstErrorPopUp from './preview-first-error-pop-up'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
function PreviewPane({
|
function PreviewPane({
|
||||||
|
@ -15,10 +16,30 @@ function PreviewPane({
|
||||||
onToggleLogs,
|
onToggleLogs,
|
||||||
outputFiles,
|
outputFiles,
|
||||||
pdfDownloadUrl,
|
pdfDownloadUrl,
|
||||||
onLogEntryLinkClick,
|
onLogEntryLocationClick,
|
||||||
showLogs
|
showLogs
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const [lastCompileTimestamp, setLastCompileTimestamp] = useState(
|
||||||
|
compilerState.lastCompileTimestamp
|
||||||
|
)
|
||||||
|
const [seenLogsForCurrentCompile, setSeenLogsForCurrentCompile] = useState(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
const [dismissedFirstErrorPopUp, setDismissedFirstErrorPopUp] = useState(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (lastCompileTimestamp < compilerState.lastCompileTimestamp) {
|
||||||
|
setLastCompileTimestamp(compilerState.lastCompileTimestamp)
|
||||||
|
setSeenLogsForCurrentCompile(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLogs && !seenLogsForCurrentCompile) {
|
||||||
|
setSeenLogsForCurrentCompile(true)
|
||||||
|
}
|
||||||
|
|
||||||
const nErrors =
|
const nErrors =
|
||||||
compilerState.logEntries && compilerState.logEntries.errors
|
compilerState.logEntries && compilerState.logEntries.errors
|
||||||
? compilerState.logEntries.errors.length
|
? compilerState.logEntries.errors.length
|
||||||
|
@ -32,6 +53,16 @@ function PreviewPane({
|
||||||
? compilerState.logEntries.all.length
|
? compilerState.logEntries.all.length
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
|
const showFirstErrorPopUp =
|
||||||
|
nErrors > 0 &&
|
||||||
|
!seenLogsForCurrentCompile &&
|
||||||
|
!dismissedFirstErrorPopUp &&
|
||||||
|
!compilerState.isCompiling
|
||||||
|
|
||||||
|
function handleFirstErrorPopUpClose() {
|
||||||
|
setDismissedFirstErrorPopUp(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PreviewToolbar
|
<PreviewToolbar
|
||||||
|
@ -58,10 +89,18 @@ function PreviewPane({
|
||||||
? t('n_warnings', { count: nWarnings })
|
? t('n_warnings', { count: nWarnings })
|
||||||
: ''}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
|
{showFirstErrorPopUp ? (
|
||||||
|
<PreviewFirstErrorPopUp
|
||||||
|
logEntry={compilerState.logEntries.errors[0]}
|
||||||
|
onGoToErrorLocation={onLogEntryLocationClick}
|
||||||
|
onViewLogs={onToggleLogs}
|
||||||
|
onClose={handleFirstErrorPopUpClose}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{showLogs ? (
|
{showLogs ? (
|
||||||
<PreviewLogsPane
|
<PreviewLogsPane
|
||||||
logEntries={compilerState.logEntries.all}
|
logEntries={compilerState.logEntries.all}
|
||||||
onLogEntryLinkClick={onLogEntryLinkClick}
|
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -74,10 +113,11 @@ PreviewPane.propTypes = {
|
||||||
isCompiling: PropTypes.bool.isRequired,
|
isCompiling: PropTypes.bool.isRequired,
|
||||||
isDraftModeOn: PropTypes.bool.isRequired,
|
isDraftModeOn: PropTypes.bool.isRequired,
|
||||||
isSyntaxCheckOn: PropTypes.bool.isRequired,
|
isSyntaxCheckOn: PropTypes.bool.isRequired,
|
||||||
|
lastCompileTimestamp: PropTypes.number,
|
||||||
logEntries: PropTypes.object.isRequired
|
logEntries: PropTypes.object.isRequired
|
||||||
}),
|
}),
|
||||||
onClearCache: PropTypes.func.isRequired,
|
onClearCache: PropTypes.func.isRequired,
|
||||||
onLogEntryLinkClick: PropTypes.func.isRequired,
|
onLogEntryLocationClick: PropTypes.func.isRequired,
|
||||||
onRecompile: PropTypes.func.isRequired,
|
onRecompile: PropTypes.func.isRequired,
|
||||||
onRunSyntaxCheckNow: PropTypes.func.isRequired,
|
onRunSyntaxCheckNow: PropTypes.func.isRequired,
|
||||||
onSetAutoCompile: PropTypes.func.isRequired,
|
onSetAutoCompile: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -31,7 +31,8 @@ export default (PdfManager = class PdfManager {
|
||||||
view: null, // 'pdf' 'logs'
|
view: null, // 'pdf' 'logs'
|
||||||
showRawLog: false,
|
showRawLog: false,
|
||||||
highlights: [],
|
highlights: [],
|
||||||
position: null
|
position: null,
|
||||||
|
lastCompileTimestamp: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -361,6 +361,7 @@ App.controller('PdfController', function(
|
||||||
if (response.status === 'success') {
|
if (response.status === 'success') {
|
||||||
$scope.pdf.view = 'pdf'
|
$scope.pdf.view = 'pdf'
|
||||||
$scope.shouldShowLogs = false
|
$scope.shouldShowLogs = false
|
||||||
|
$scope.pdf.lastCompileTimestamp = Date.now()
|
||||||
|
|
||||||
// define the base url. if the pdf file has a build number, pass it to the clsi in the url
|
// 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) {
|
if (fileByPath['output.pdf'] && fileByPath['output.pdf'].url) {
|
||||||
|
@ -516,6 +517,8 @@ App.controller('PdfController', function(
|
||||||
|
|
||||||
function fetchLogs(fileByPath, options) {
|
function fetchLogs(fileByPath, options) {
|
||||||
let blgFile, chktexFile, logFile
|
let blgFile, chktexFile, logFile
|
||||||
|
$scope.pdf.logEntries = {}
|
||||||
|
|
||||||
if (options != null ? options.validation : undefined) {
|
if (options != null ? options.validation : undefined) {
|
||||||
chktexFile = fileByPath['output.chktex']
|
chktexFile = fileByPath['output.chktex']
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -111,6 +111,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bounce {
|
.bounce {
|
||||||
-webkit-animation-duration: 2s;
|
-webkit-animation-duration: 2s;
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
|
|
|
@ -118,10 +118,7 @@
|
||||||
.log-entry-btn-expand-collapse {
|
.log-entry-btn-expand-collapse {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
&:focus,
|
.no-outline-ring-on-click;
|
||||||
&:focus:active {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-entry-human-readable-hint,
|
.log-entry-human-readable-hint,
|
||||||
|
@ -129,3 +126,33 @@
|
||||||
font-size: @font-size-small;
|
font-size: @font-size-small;
|
||||||
margin-top: @margin-sm;
|
margin-top: @margin-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.first-error-popup {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: @toolbar-small-height + 2px;
|
||||||
|
right: @padding-xs;
|
||||||
|
border-radius: @border-radius-base;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 450px;
|
||||||
|
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
animation: fade-in 0.15s linear 0s 1 none;
|
||||||
|
background-color: #fff;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
.triangle(top, @padding-sm, @padding-xs, @ol-red);
|
||||||
|
top: -@padding-xs;
|
||||||
|
right: @padding-md;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-error-popup-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 @padding-sm @padding-sm @padding-sm;
|
||||||
|
margin-top: -@margin-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-error-btn {
|
||||||
|
.no-outline-ring-on-click;
|
||||||
|
}
|
||||||
|
|
|
@ -64,38 +64,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.triangle(@_, @width, @height, @color) {
|
|
||||||
position: absolute;
|
|
||||||
border-color: transparent;
|
|
||||||
border-style: solid;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
.triangle(top, @width, @height, @color) {
|
|
||||||
border-width: 0 @width / 2 @height @width / 2;
|
|
||||||
border-bottom-color: @color;
|
|
||||||
border-left-color: transparent;
|
|
||||||
border-right-color: transparent;
|
|
||||||
}
|
|
||||||
.triangle(bottom, @width, @height, @color) {
|
|
||||||
border-width: @height @width / 2 0 @width / 2;
|
|
||||||
border-top-color: @color;
|
|
||||||
border-left-color: transparent;
|
|
||||||
border-right-color: transparent;
|
|
||||||
}
|
|
||||||
.triangle(right, @width, @height, @color) {
|
|
||||||
border-width: @height / 2 0 @height / 2 @width;
|
|
||||||
border-left-color: @color;
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
.triangle(left, @width, @height, @color) {
|
|
||||||
border-width: @height / 2 @width @height / 2 0;
|
|
||||||
border-right-color: @color;
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
#review-panel {
|
#review-panel {
|
||||||
display: block;
|
display: block;
|
||||||
.rp-size-expanded & {
|
.rp-size-expanded & {
|
||||||
|
|
|
@ -1,22 +1,5 @@
|
||||||
@import './list/v1-import-modal.less';
|
@import './list/v1-import-modal.less';
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes fade-in {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-list-page {
|
.project-list-page {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: @header-height;
|
top: @header-height;
|
||||||
|
|
|
@ -1095,3 +1095,42 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.triangle(@_, @width, @height, @color) {
|
||||||
|
position: absolute;
|
||||||
|
border-color: transparent;
|
||||||
|
border-style: solid;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.triangle(top, @width, @height, @color) {
|
||||||
|
border-width: 0 @width / 2 @height @width / 2;
|
||||||
|
border-bottom-color: @color;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
.triangle(bottom, @width, @height, @color) {
|
||||||
|
border-width: @height @width / 2 0 @width / 2;
|
||||||
|
border-top-color: @color;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
.triangle(right, @width, @height, @color) {
|
||||||
|
border-width: @height / 2 0 @height / 2 @width;
|
||||||
|
border-left-color: @color;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
.triangle(left, @width, @height, @color) {
|
||||||
|
border-width: @height / 2 @width @height / 2 0;
|
||||||
|
border-right-color: @color;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-outline-ring-on-click {
|
||||||
|
&:focus,
|
||||||
|
&:focus:active {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
{
|
{
|
||||||
|
"first_error_popup_label": "Your project has errors. This is the first one.",
|
||||||
|
"dismiss_error_popup": "Dismiss first error alert",
|
||||||
|
"go_to_error_location": "Go to error location",
|
||||||
|
"view_all_errors": "View all errors",
|
||||||
"log_entry_description": "Log entry with level: \"__level__\"",
|
"log_entry_description": "Log entry with level: \"__level__\"",
|
||||||
"navigate_log_source": "Navigate to log position in source code: __location__",
|
"navigate_log_source": "Navigate to log position in source code: __location__",
|
||||||
"other_output_files": "Other output files",
|
"other_output_files": "Other output files",
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
describe('log entry description', function() {
|
describe('log entry description', function() {
|
||||||
for (const level of ['error', 'warning', 'typesetting']) {
|
for (const level of ['error', 'warning', 'typesetting']) {
|
||||||
it(`describes the log entry with ${level} information`, function() {
|
it(`describes the log entry with ${level} information`, function() {
|
||||||
render(<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />)
|
render(<PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />)
|
||||||
screen.getByLabelText(`Log entry with level: "${level}"`)
|
screen.getByLabelText(`Log entry with level: "${level}"`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,10 @@ describe('<PreviewLogEntry />', function() {
|
||||||
const file = 'foo.tex'
|
const file = 'foo.tex'
|
||||||
const line = 42
|
const line = 42
|
||||||
const column = 21
|
const column = 21
|
||||||
const onLogEntryLinkClick = sinon.stub()
|
const onLogEntryLocationClick = sinon.stub()
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
onLogEntryLinkClick.reset()
|
onLogEntryLocationClick.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders both file and line', function() {
|
it('renders both file and line', function() {
|
||||||
|
@ -34,7 +34,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
file={file}
|
file={file}
|
||||||
line={line}
|
line={line}
|
||||||
level={level}
|
level={level}
|
||||||
onLogEntryLinkClick={noOp}
|
onLogEntryLocationClick={noOp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
screen.getByRole('button', {
|
screen.getByRole('button', {
|
||||||
|
@ -44,7 +44,11 @@ describe('<PreviewLogEntry />', function() {
|
||||||
|
|
||||||
it('renders only file when line information is not available', function() {
|
it('renders only file when line information is not available', function() {
|
||||||
render(
|
render(
|
||||||
<PreviewLogEntry file={file} level={level} onLogEntryLinkClick={noOp} />
|
<PreviewLogEntry
|
||||||
|
file={file}
|
||||||
|
level={level}
|
||||||
|
onLogEntryLocationClick={noOp}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
screen.getByRole('button', {
|
screen.getByRole('button', {
|
||||||
name: `Navigate to log position in source code: ${file}`
|
name: `Navigate to log position in source code: ${file}`
|
||||||
|
@ -52,7 +56,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render when file information is not available', function() {
|
it('does not render when file information is not available', function() {
|
||||||
render(<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />)
|
render(<PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />)
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole('button', {
|
screen.queryByRole('button', {
|
||||||
name: `Navigate to log position in source code: `
|
name: `Navigate to log position in source code: `
|
||||||
|
@ -67,7 +71,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
line={line}
|
line={line}
|
||||||
column={column}
|
column={column}
|
||||||
level={level}
|
level={level}
|
||||||
onLogEntryLinkClick={onLogEntryLinkClick}
|
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
const linkToSourceButton = screen.getByRole('button', {
|
const linkToSourceButton = screen.getByRole('button', {
|
||||||
|
@ -75,8 +79,8 @@ describe('<PreviewLogEntry />', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.click(linkToSourceButton)
|
fireEvent.click(linkToSourceButton)
|
||||||
expect(onLogEntryLinkClick).to.be.calledOnce
|
expect(onLogEntryLocationClick).to.be.calledOnce
|
||||||
expect(onLogEntryLinkClick).to.be.calledWith({
|
expect(onLogEntryLocationClick).to.be.calledWith({
|
||||||
file,
|
file,
|
||||||
line: line,
|
line: line,
|
||||||
column: column
|
column: column
|
||||||
|
@ -92,7 +96,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
<PreviewLogEntry
|
<PreviewLogEntry
|
||||||
content={logContent}
|
content={logContent}
|
||||||
level={level}
|
level={level}
|
||||||
onLogEntryLinkClick={noOp}
|
onLogEntryLocationClick={noOp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
screen.getByText(logContent)
|
screen.getByText(logContent)
|
||||||
|
@ -106,7 +110,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
<PreviewLogEntry
|
<PreviewLogEntry
|
||||||
content={logContent}
|
content={logContent}
|
||||||
level={level}
|
level={level}
|
||||||
onLogEntryLinkClick={noOp}
|
onLogEntryLocationClick={noOp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
screen.getByText(logContent)
|
screen.getByText(logContent)
|
||||||
|
@ -121,7 +125,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
|
|
||||||
it('should not render at all when there are no log contents', function() {
|
it('should not render at all when there are no log contents', function() {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />
|
<PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />
|
||||||
)
|
)
|
||||||
expect(container.querySelector('.log-entry-content')).to.not.exist
|
expect(container.querySelector('.log-entry-content')).to.not.exist
|
||||||
})
|
})
|
||||||
|
@ -140,7 +144,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
humanReadableHintComponent={logHint}
|
humanReadableHintComponent={logHint}
|
||||||
extraInfoURL={infoURL}
|
extraInfoURL={infoURL}
|
||||||
level={level}
|
level={level}
|
||||||
onLogEntryLinkClick={noOp}
|
onLogEntryLocationClick={noOp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
screen.getByText(logHintText)
|
screen.getByText(logHintText)
|
||||||
|
@ -153,7 +157,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
humanReadableHintComponent={logHint}
|
humanReadableHintComponent={logHint}
|
||||||
extraInfoURL={infoURL}
|
extraInfoURL={infoURL}
|
||||||
level={level}
|
level={level}
|
||||||
onLogEntryLinkClick={noOp}
|
onLogEntryLocationClick={noOp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
screen.getByRole('link', { name: 'Learn more' })
|
screen.getByRole('link', { name: 'Learn more' })
|
||||||
|
@ -165,7 +169,7 @@ describe('<PreviewLogEntry />', function() {
|
||||||
content={logContent}
|
content={logContent}
|
||||||
humanReadableHintComponent={logHint}
|
humanReadableHintComponent={logHint}
|
||||||
level={level}
|
level={level}
|
||||||
onLogEntryLinkClick={noOp}
|
onLogEntryLocationClick={noOp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist
|
expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { screen, render, fireEvent } from '@testing-library/react'
|
||||||
|
import PreviewPane from '../../../../../frontend/js/features/preview/components/preview-pane'
|
||||||
|
const { expect } = require('chai')
|
||||||
|
|
||||||
|
describe('<PreviewPane />', function() {
|
||||||
|
const sampleError1 = {
|
||||||
|
content: 'error 1 content',
|
||||||
|
file: 'main.tex',
|
||||||
|
level: 'error',
|
||||||
|
line: 17,
|
||||||
|
message: 'Misplaced alignment tab character &.'
|
||||||
|
}
|
||||||
|
const sampleError2 = {
|
||||||
|
content: 'error 1 content',
|
||||||
|
file: 'main.tex',
|
||||||
|
level: 'error',
|
||||||
|
line: 22,
|
||||||
|
message: 'Extra alignment tab has been changed to cr.'
|
||||||
|
}
|
||||||
|
const sampleWarning = {
|
||||||
|
file: 'main.tex',
|
||||||
|
level: 'warning',
|
||||||
|
line: 30,
|
||||||
|
message: "Reference `idontexist' on page 1 undefined on input line 30."
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('first error pop-up', function() {
|
||||||
|
it('renders a first error pop-up with the first error', function() {
|
||||||
|
const propsAfterCompileWithErrors = getProps(false, {
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
})
|
||||||
|
render(<PreviewPane {...propsAfterCompileWithErrors} />)
|
||||||
|
screen.getByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
screen.getByText(sampleError1.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render a first error pop-up when there are only warnings', function() {
|
||||||
|
const propsAfterCompileWithWarningsOnly = getProps(false, {
|
||||||
|
errors: [],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
})
|
||||||
|
render(<PreviewPane {...propsAfterCompileWithWarningsOnly} />)
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render a first error pop-up when a compile is ongoing', function() {
|
||||||
|
const propsWhileCompiling = getProps(true, {
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
})
|
||||||
|
render(<PreviewPane {...propsWhileCompiling} />)
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render a first error pop-up when viewing logs', function() {
|
||||||
|
const propsWithErrorsViewingLogs = getProps(
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
render(<PreviewPane {...propsWithErrorsViewingLogs} />)
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render a first error pop-up when going back to the PDF view after viewing logs', function() {
|
||||||
|
const nowTimestamp = Date.now()
|
||||||
|
const propsWithErrorsViewingLogs = getProps(
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
},
|
||||||
|
nowTimestamp,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const propsWithErrorsAfterViewingLogs = getProps(
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
},
|
||||||
|
nowTimestamp,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
const { rerender } = render(
|
||||||
|
<PreviewPane {...propsWithErrorsViewingLogs} />
|
||||||
|
)
|
||||||
|
rerender(<PreviewPane {...propsWithErrorsAfterViewingLogs} />)
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a first error pop-up with updated errors after recompiling', function() {
|
||||||
|
const nowTimestamp = Date.now()
|
||||||
|
const laterTimestamp = Date.now() + 1000
|
||||||
|
const propsWithErrorsAfterFirstCompile = getProps(
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
},
|
||||||
|
nowTimestamp,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const propsWithErrorsAfterSecondCompile = getProps(
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
errors: [sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
},
|
||||||
|
laterTimestamp,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
const { rerender } = render(
|
||||||
|
<PreviewPane {...propsWithErrorsAfterFirstCompile} />
|
||||||
|
)
|
||||||
|
rerender(<PreviewPane {...propsWithErrorsAfterSecondCompile} />)
|
||||||
|
screen.getByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
screen.getByText(sampleError2.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows dismissing the first error pop-up', function() {
|
||||||
|
const propsWithErrors = getProps(false, {
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
})
|
||||||
|
render(<PreviewPane {...propsWithErrors} />)
|
||||||
|
const dismissPopUpButton = screen.getByRole('button', {
|
||||||
|
name: 'Dismiss first error alert'
|
||||||
|
})
|
||||||
|
|
||||||
|
fireEvent.click(dismissPopUpButton)
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render the first error pop-up with new recompiles after it being dismissed once', function() {
|
||||||
|
const nowTimestamp = Date.now()
|
||||||
|
const laterTimestamp = Date.now() + 1000
|
||||||
|
const propsWithErrorsForFirstCompile = getProps(
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
errors: [sampleError1, sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
},
|
||||||
|
nowTimestamp
|
||||||
|
)
|
||||||
|
const propsWithErrorsForSecondCompile = getProps(
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
errors: [sampleError2],
|
||||||
|
warnings: [sampleWarning]
|
||||||
|
},
|
||||||
|
laterTimestamp
|
||||||
|
)
|
||||||
|
const { rerender } = render(
|
||||||
|
<PreviewPane {...propsWithErrorsForFirstCompile} />
|
||||||
|
)
|
||||||
|
const dismissPopUpButton = screen.getByRole('button', {
|
||||||
|
name: 'Dismiss first error alert'
|
||||||
|
})
|
||||||
|
fireEvent.click(dismissPopUpButton)
|
||||||
|
rerender(<PreviewPane {...propsWithErrorsForSecondCompile} />)
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('alertdialog', {
|
||||||
|
name: 'Your project has errors. This is the first one.'
|
||||||
|
})
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function getProps(
|
||||||
|
isCompiling = false,
|
||||||
|
logEntries = {},
|
||||||
|
lastCompileTimestamp = Date.now(),
|
||||||
|
isShowingLogs = false
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
compilerState: {
|
||||||
|
isAutoCompileOn: false,
|
||||||
|
isCompiling: isCompiling,
|
||||||
|
isClearingCache: false,
|
||||||
|
isDraftModeOn: false,
|
||||||
|
isSyntaxCheckOn: false,
|
||||||
|
lastCompileTimestamp: lastCompileTimestamp,
|
||||||
|
logEntries: logEntries
|
||||||
|
},
|
||||||
|
onClearCache: () => {},
|
||||||
|
onLogEntryLocationClick: () => {},
|
||||||
|
onRecompile: () => {},
|
||||||
|
onRunSyntaxCheckNow: () => {},
|
||||||
|
onSetAutoCompile: () => {},
|
||||||
|
onSetDraftMode: () => {},
|
||||||
|
onSetSyntaxCheck: () => {},
|
||||||
|
onToggleLogs: () => {},
|
||||||
|
showLogs: isShowingLogs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in a new issue