mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 12:03:33 -05:00
Log pane improvements (#3418)
* Ordering of log entries in the new errors UI * Don't show the expand-collapse widget when not needed; smaller font size in the raw log output * Expose log actions in the log pane. * Use "This project" instead of "Your project" in the new errors UI * Better handling of long log messages; left-ellipsize the file/line number button * Make log location more button-like; add tooltip when needed. * Add a PDF expand button to the toolbar. * Add a stop compilation button to the new compile UI * Use aria-label for button accessible text; improve handling of long filenames in the log location button * Set max-height correctly for the logs pane dropdown * Avoid changing raw logs sizing when expanded and collapsed * Add comment on the solution for right-to-left text and ellipsis * Improve logs pane actions GitOrigin-RevId: 4098d77a9ee6d333644906876b9ff27035b79319
This commit is contained in:
parent
475b51d21e
commit
4e74fb2694
28 changed files with 888 additions and 418 deletions
|
@ -16,6 +16,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
}`
|
||||
on-clear-cache="clearCache"
|
||||
on-recompile="recompile"
|
||||
on-recompile-from-scratch="recompileFromScratch"
|
||||
on-run-syntax-check-now="runSyntaxCheckNow"
|
||||
on-set-auto-compile="setAutoCompile"
|
||||
on-set-draft-mode="setDraftMode"
|
||||
|
@ -23,6 +24,10 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
on-toggle-logs="toggleLogs"
|
||||
output-files="pdf.outputFiles"
|
||||
pdf-download-url="pdf.downloadUrl"
|
||||
split-layout="ui.pdfLayout === 'sideBySide'"
|
||||
on-set-split-layout="setPdfSplitLayout"
|
||||
on-set-full-layout="setPdfFullLayout"
|
||||
on-stop-compilation="stop"
|
||||
show-logs="shouldShowLogs"
|
||||
on-log-entry-location-click="openInEditor"
|
||||
)
|
||||
|
@ -234,7 +239,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
href
|
||||
dropdown-toggle
|
||||
)
|
||||
| !{translate("other_logs_and_files")}
|
||||
| #{translate("other_logs_and_files")}
|
||||
span.caret
|
||||
ul.dropdown-menu.dropdown-menu-right
|
||||
li(ng-repeat="file in pdf.outputFiles")
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"auto_compile",
|
||||
"autocompile_disabled_reason",
|
||||
"autocompile_disabled",
|
||||
"clear_cached_files",
|
||||
"clsi_maintenance",
|
||||
"clsi_unavailable",
|
||||
"collapse",
|
||||
|
@ -21,6 +22,7 @@
|
|||
"find_out_more_about_the_file_outline",
|
||||
"first_error_popup_label",
|
||||
"following_paths_conflict",
|
||||
"full_screen",
|
||||
"go_to_error_location",
|
||||
"hide_outline",
|
||||
"ignore_validation_errors",
|
||||
|
@ -39,6 +41,7 @@
|
|||
"normal",
|
||||
"off",
|
||||
"on",
|
||||
"other_logs_and_files",
|
||||
"other_output_files",
|
||||
"pdf_compile_in_progress_error",
|
||||
"pdf_compile_rate_limit_hit",
|
||||
|
@ -59,6 +62,8 @@
|
|||
"show_outline",
|
||||
"something_went_wrong_rendering_pdf",
|
||||
"somthing_went_wrong_compiling",
|
||||
"split_screen",
|
||||
"stop_compile",
|
||||
"stop_on_validation_error",
|
||||
"terminated",
|
||||
"timedout",
|
||||
|
|
|
@ -1,36 +1,18 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import { Dropdown, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewDownloadFileList from './preview-download-file-list'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
export const topFileTypes = ['bbl', 'gls', 'ind']
|
||||
|
||||
function PreviewDownloadButton({
|
||||
isCompiling,
|
||||
outputFiles,
|
||||
pdfDownloadUrl,
|
||||
showText
|
||||
}) {
|
||||
let topFiles = []
|
||||
let otherFiles = []
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (outputFiles) {
|
||||
topFiles = outputFiles.filter(file => {
|
||||
if (topFileTypes.includes(file.type)) {
|
||||
return file
|
||||
}
|
||||
})
|
||||
|
||||
otherFiles = outputFiles.filter(file => {
|
||||
if (!topFileTypes.includes(file.type)) {
|
||||
if (file.type === 'pdf' && file.main === true) return
|
||||
return file
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let textStyle = {}
|
||||
if (!showText) {
|
||||
textStyle = {
|
||||
|
@ -77,37 +59,12 @@ function PreviewDownloadButton({
|
|||
bsStyle="info"
|
||||
/>
|
||||
<Dropdown.Menu id="download-dropdown-list">
|
||||
<MenuItem header>{t('other_output_files')}</MenuItem>
|
||||
<FileList list={topFiles} listType="main" />
|
||||
{otherFiles.length > 0 && topFiles.length > 0 ? (
|
||||
<>
|
||||
<MenuItem divider />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{otherFiles.length > 0 ? (
|
||||
<>
|
||||
<FileList list={otherFiles} listType="other" />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<PreviewDownloadFileList fileList={outputFiles} />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function FileList({ listType, list }) {
|
||||
return list.map((file, index) => {
|
||||
return (
|
||||
<MenuItem download href={file.url} key={`${listType}-${index}`}>
|
||||
<b>{file.fileName}</b>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
PreviewDownloadButton.propTypes = {
|
||||
isCompiling: PropTypes.bool.isRequired,
|
||||
outputFiles: PropTypes.array,
|
||||
|
@ -115,9 +72,4 @@ PreviewDownloadButton.propTypes = {
|
|||
showText: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
FileList.propTypes = {
|
||||
list: PropTypes.array.isRequired,
|
||||
listType: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default PreviewDownloadButton
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { MenuItem } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const topFileTypes = ['bbl', 'gls', 'ind']
|
||||
|
||||
function PreviewDownloadFileList({ fileList = [] }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
let topFiles = []
|
||||
let otherFiles = []
|
||||
|
||||
if (fileList) {
|
||||
topFiles = fileList.filter(file => {
|
||||
if (topFileTypes.includes(file.type)) {
|
||||
return file
|
||||
}
|
||||
})
|
||||
|
||||
otherFiles = fileList.filter(file => {
|
||||
if (!topFileTypes.includes(file.type)) {
|
||||
if (file.type === 'pdf' && file.main === true) return
|
||||
return file
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem header>{t('other_output_files')}</MenuItem>
|
||||
<SubFileList subFileList={topFiles} listType="main" />
|
||||
{otherFiles.length > 0 && topFiles.length > 0 ? (
|
||||
<>
|
||||
<MenuItem divider />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{otherFiles.length > 0 ? (
|
||||
<>
|
||||
<SubFileList subFileList={otherFiles} listType="other" />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SubFileList({ subFileList, listType }) {
|
||||
return subFileList.map((file, index) => {
|
||||
return (
|
||||
<MenuItem download href={file.url} key={`${listType}${index}`}>
|
||||
<b>{file.fileName}</b>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
SubFileList.propTypes = {
|
||||
subFileList: PropTypes.array.isRequired,
|
||||
listType: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
PreviewDownloadFileList.propTypes = {
|
||||
fileList: PropTypes.array
|
||||
}
|
||||
|
||||
export default PreviewDownloadFileList
|
|
@ -31,6 +31,7 @@ function PreviewFirstErrorPopUp({
|
|||
level={logEntry.level}
|
||||
showLineAndNoLink={false}
|
||||
showCloseButton
|
||||
customClass="log-entry-first-error-popup"
|
||||
onClose={onClose}
|
||||
/>
|
||||
<div className="first-error-popup-actions">
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react'
|
||||
import React, { useState, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import useExpandCollapse from '../../../shared/hooks/use-expand-collapse'
|
||||
import useResizeObserver from '../../../shared/hooks/use-resize-observer'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
function PreviewLogsPaneEntry({
|
||||
|
@ -15,15 +17,19 @@ function PreviewLogsPaneEntry({
|
|||
showSourceLocationLink = true,
|
||||
showCloseButton = false,
|
||||
entryAriaLabel = null,
|
||||
customClass,
|
||||
onSourceLocationClick,
|
||||
onClose
|
||||
}) {
|
||||
function handleLogEntryLinkClick() {
|
||||
const logEntryClasses = classNames('log-entry', customClass)
|
||||
|
||||
function handleLogEntryLinkClick(e) {
|
||||
e.preventDefault()
|
||||
onSourceLocationClick(sourceLocation)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="log-entry" aria-label={entryAriaLabel}>
|
||||
<div className={logEntryClasses} aria-label={entryAriaLabel}>
|
||||
<PreviewLogEntryHeader
|
||||
level={level}
|
||||
sourceLocation={sourceLocation}
|
||||
|
@ -54,6 +60,15 @@ function PreviewLogEntryHeader({
|
|||
onClose
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const logLocationSpanRef = useRef()
|
||||
const [showLocationTooltip, setShowLocationTooltip] = useState(false)
|
||||
|
||||
useResizeObserver(
|
||||
logLocationSpanRef,
|
||||
showLocationTooltip,
|
||||
setTooltipForLogLocationLinkIfNeeded
|
||||
)
|
||||
|
||||
const file = sourceLocation ? sourceLocation.file : null
|
||||
const line = sourceLocation ? sourceLocation.line : null
|
||||
const logEntryHeaderClasses = classNames('log-entry-header', {
|
||||
|
@ -62,26 +77,69 @@ function PreviewLogEntryHeader({
|
|||
'log-entry-header-typesetting': level === 'typesetting',
|
||||
'log-entry-header-raw': level === 'raw'
|
||||
})
|
||||
const logEntryLocationBtnClasses = classNames('log-entry-header-link', {
|
||||
'log-entry-header-link-error': level === 'error',
|
||||
'log-entry-header-link-warning': level === 'warning',
|
||||
'log-entry-header-link-typesetting': level === 'typesetting',
|
||||
'log-entry-header-link-raw': level === 'raw'
|
||||
})
|
||||
const headerLogLocationTitle = t('navigate_log_source', {
|
||||
location: file + (line ? `, ${line}` : '')
|
||||
})
|
||||
|
||||
function setTooltipForLogLocationLinkIfNeeded(observedElement) {
|
||||
const spanEl = observedElement.target
|
||||
const shouldShowTooltip = spanEl.scrollWidth > spanEl.clientWidth
|
||||
setShowLocationTooltip(shouldShowTooltip)
|
||||
}
|
||||
|
||||
const locationLinkText =
|
||||
showSourceLocationLink && file ? `${file}${line ? `, ${line}` : ''}` : null
|
||||
|
||||
// Because we want an ellipsis on the left-hand side (e.g. "...longfilename.tex"), the
|
||||
// `log-entry-header-link-location` class has text laid out from right-to-left using the CSS
|
||||
// rule `direction: rtl;`.
|
||||
// This works most of the times, except when the first character of the filename is considered
|
||||
// a punctuation mark, like `/` (e.g. `/foo/bar/baz.sty`). In this case, because of
|
||||
// right-to-left writing rules, the punctuation mark is moved to the right-side of the string,
|
||||
// resulting in `...bar/baz.sty/` instead of `...bar/baz.sty`.
|
||||
// To avoid this edge-case, we wrap the `logLocationLinkText` in two directional formatting
|
||||
// characters:
|
||||
// * \u202A LEFT-TO-RIGHT EMBEDDING Treat the following text as embedded left-to-right.
|
||||
// * \u202C POP DIRECTIONAL FORMATTING End the scope of the last LRE, RLE, RLO, or LRO.
|
||||
// This essentially tells the browser that, althought the text is laid out from right-to-left,
|
||||
// the wrapped portion of text should follow left-to-right writing rules.
|
||||
const locationLink = locationLinkText ? (
|
||||
<button
|
||||
className={logEntryLocationBtnClasses}
|
||||
type="button"
|
||||
aria-label={headerLogLocationTitle}
|
||||
onClick={onSourceLocationClick}
|
||||
>
|
||||
<Icon type="chain" />
|
||||
|
||||
<span ref={logLocationSpanRef} className="log-entry-header-link-location">
|
||||
{`\u202A${locationLinkText}\u202C`}
|
||||
</span>
|
||||
</button>
|
||||
) : null
|
||||
|
||||
const locationTooltip = showLocationTooltip ? (
|
||||
<Tooltip id={locationLinkText} className="log-location-tooltip">
|
||||
{locationLinkText}
|
||||
</Tooltip>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<header className={logEntryHeaderClasses}>
|
||||
<h3 className="log-entry-header-title">{headerTitle}</h3>
|
||||
{showSourceLocationLink && file ? (
|
||||
<button
|
||||
className="btn-inline-link log-entry-header-link"
|
||||
type="button"
|
||||
title={headerLogLocationTitle}
|
||||
onClick={onSourceLocationClick}
|
||||
>
|
||||
<Icon type="chain" />
|
||||
|
||||
<span>{file}</span>
|
||||
{line ? <span>, {line}</span> : null}
|
||||
</button>
|
||||
) : null}
|
||||
{showLocationTooltip ? (
|
||||
<OverlayTrigger placement="left" overlay={locationTooltip}>
|
||||
{locationLink}
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
locationLink
|
||||
)}
|
||||
{showCloseButton ? (
|
||||
<button
|
||||
className="btn-inline-link log-entry-header-link"
|
||||
|
@ -101,45 +159,50 @@ function PreviewLogEntryContent({
|
|||
formattedContent,
|
||||
extraInfoURL
|
||||
}) {
|
||||
const { isExpanded, expandableProps, toggleProps } = useExpandCollapse({
|
||||
collapsedSize: 150,
|
||||
classes: {
|
||||
container: 'log-entry-content-raw-expandable-container'
|
||||
}
|
||||
})
|
||||
const logContentClasses = classNames('log-entry-content-raw', {
|
||||
'log-entry-content-raw-collapsed': !isExpanded
|
||||
const {
|
||||
isExpanded,
|
||||
needsExpandCollapse,
|
||||
expandableProps,
|
||||
toggleProps
|
||||
} = useExpandCollapse({
|
||||
collapsedSize: 150
|
||||
})
|
||||
|
||||
const buttonContainerClasses = classNames(
|
||||
'log-entry-content-button-container',
|
||||
{
|
||||
'log-entry-content-button-container-collapsed': !isExpanded
|
||||
}
|
||||
)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="log-entry-content">
|
||||
{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 className="log-entry-content-raw-container">
|
||||
<div {...expandableProps}>
|
||||
<pre className="log-entry-content-raw">{rawContent.trim()}</pre>
|
||||
</div>
|
||||
{needsExpandCollapse ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{formattedContent ? (
|
||||
|
@ -186,6 +249,7 @@ PreviewLogsPaneEntry.propTypes = {
|
|||
formattedContent: PropTypes.node,
|
||||
extraInfoURL: PropTypes.string,
|
||||
level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw']).isRequired,
|
||||
customClass: PropTypes.string,
|
||||
showSourceLocationLink: PropTypes.bool,
|
||||
showCloseButton: PropTypes.bool,
|
||||
entryAriaLabel: PropTypes.string,
|
||||
|
|
|
@ -1,18 +1,31 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
|
||||
import PreviewValidationIssue from './preview-validation-issue'
|
||||
import PreviewDownloadFileList from './preview-download-file-list'
|
||||
import PreviewError from './preview-error'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
function PreviewLogsPane({
|
||||
logEntries = [],
|
||||
logEntries = { all: [], errors: [], warnings: [], typesetting: [] },
|
||||
rawLog = '',
|
||||
validationIssues = {},
|
||||
errors = {},
|
||||
onLogEntryLocationClick
|
||||
outputFiles = [],
|
||||
isClearingCache,
|
||||
isCompiling = false,
|
||||
onLogEntryLocationClick,
|
||||
onClearCache
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
all: allCompilerIssues = [],
|
||||
errors: compilerErrors = [],
|
||||
warnings: compilerWarnings = [],
|
||||
typesetting: compilerTypesettingIssues = []
|
||||
} = logEntries
|
||||
|
||||
const errorsUI = Object.keys(errors).map((name, index) => (
|
||||
<PreviewError key={index} name={name} />
|
||||
|
@ -28,7 +41,11 @@ function PreviewLogsPane({
|
|||
)
|
||||
)
|
||||
|
||||
const logEntriesUI = logEntries.map((logEntry, idx) => (
|
||||
const logEntriesUI = [
|
||||
...compilerErrors,
|
||||
...compilerWarnings,
|
||||
...compilerTypesettingIssues
|
||||
].map((logEntry, idx) => (
|
||||
<PreviewLogsPaneEntry
|
||||
key={idx}
|
||||
headerTitle={logEntry.message}
|
||||
|
@ -48,6 +65,39 @@ function PreviewLogsPane({
|
|||
/>
|
||||
))
|
||||
|
||||
const actionsUI = (
|
||||
<div className="logs-pane-actions">
|
||||
<button
|
||||
className="btn btn-sm btn-danger logs-pane-actions-clear-cache"
|
||||
onClick={onClearCache}
|
||||
disabled={isClearingCache || isCompiling}
|
||||
>
|
||||
{isClearingCache ? (
|
||||
<Icon type="refresh" spin />
|
||||
) : (
|
||||
<Icon type="trash-o" />
|
||||
)}
|
||||
|
||||
<span>{t('clear_cached_files')}</span>
|
||||
</button>
|
||||
<Dropdown
|
||||
id="dropdown-files-logs-pane"
|
||||
dropup
|
||||
pullRight
|
||||
disabled={isCompiling}
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
className="btn btn-sm btn-info dropdown-toggle"
|
||||
title={t('other_logs_and_files')}
|
||||
bsStyle="info"
|
||||
/>
|
||||
<Dropdown.Menu id="dropdown-files-logs-pane-list">
|
||||
<PreviewDownloadFileList fileList={outputFiles} />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
|
||||
const rawLogUI = (
|
||||
<PreviewLogsPaneEntry
|
||||
headerTitle={t('raw_logs')}
|
||||
|
@ -59,18 +109,30 @@ function PreviewLogsPane({
|
|||
|
||||
return (
|
||||
<div className="logs-pane">
|
||||
{errors ? errorsUI : null}
|
||||
{validationIssues ? validationIssuesUI : null}
|
||||
{logEntries ? logEntriesUI : null}
|
||||
{rawLog && rawLog !== '' ? rawLogUI : null}
|
||||
<div className="logs-pane-content">
|
||||
{errors ? errorsUI : null}
|
||||
{validationIssues ? validationIssuesUI : null}
|
||||
{allCompilerIssues.length > 0 ? logEntriesUI : null}
|
||||
{rawLog && rawLog !== '' ? rawLogUI : null}
|
||||
{actionsUI}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
PreviewLogsPane.propTypes = {
|
||||
logEntries: PropTypes.array,
|
||||
logEntries: PropTypes.shape({
|
||||
all: PropTypes.array,
|
||||
errors: PropTypes.array,
|
||||
warning: PropTypes.array,
|
||||
typesetting: PropTypes.array
|
||||
}),
|
||||
rawLog: PropTypes.string,
|
||||
outputFiles: PropTypes.array,
|
||||
isClearingCache: PropTypes.bool,
|
||||
isCompiling: PropTypes.bool,
|
||||
onLogEntryLocationClick: PropTypes.func.isRequired,
|
||||
onClearCache: PropTypes.func.isRequired,
|
||||
validationIssues: PropTypes.object,
|
||||
errors: PropTypes.object
|
||||
}
|
||||
|
|
|
@ -9,15 +9,20 @@ function PreviewPane({
|
|||
compilerState,
|
||||
onClearCache,
|
||||
onRecompile,
|
||||
onRecompileFromScratch,
|
||||
onRunSyntaxCheckNow,
|
||||
onSetAutoCompile,
|
||||
onSetDraftMode,
|
||||
onSetSyntaxCheck,
|
||||
onToggleLogs,
|
||||
onSetFullLayout,
|
||||
onSetSplitLayout,
|
||||
onStopCompilation,
|
||||
outputFiles,
|
||||
pdfDownloadUrl,
|
||||
onLogEntryLocationClick,
|
||||
showLogs
|
||||
showLogs,
|
||||
splitLayout
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
@ -81,15 +86,19 @@ function PreviewPane({
|
|||
compilerState={compilerState}
|
||||
logsState={{ nErrors, nWarnings, nLogEntries }}
|
||||
showLogs={showLogs}
|
||||
onClearCache={onClearCache}
|
||||
onRecompile={onRecompile}
|
||||
onRecompileFromScratch={onRecompileFromScratch}
|
||||
onRunSyntaxCheckNow={onRunSyntaxCheckNow}
|
||||
onSetAutoCompile={onSetAutoCompile}
|
||||
onSetDraftMode={onSetDraftMode}
|
||||
onSetSyntaxCheck={onSetSyntaxCheck}
|
||||
onToggleLogs={onToggleLogs}
|
||||
onSetSplitLayout={onSetSplitLayout}
|
||||
onSetFullLayout={onSetFullLayout}
|
||||
onStopCompilation={onStopCompilation}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
splitLayout={splitLayout}
|
||||
/>
|
||||
<span aria-live="polite" className="sr-only">
|
||||
{hasCLSIErrors ? t('compile_error_description') : ''}
|
||||
|
@ -117,11 +126,15 @@ function PreviewPane({
|
|||
) : null}
|
||||
{showLogs ? (
|
||||
<PreviewLogsPane
|
||||
logEntries={compilerState.logEntries.all}
|
||||
logEntries={compilerState.logEntries}
|
||||
rawLog={compilerState.rawLog}
|
||||
validationIssues={compilerState.validationIssues}
|
||||
errors={compilerState.errors}
|
||||
outputFiles={outputFiles}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
isClearingCache={compilerState.isClearingCache}
|
||||
isCompiling={compilerState.isCompiling}
|
||||
onClearCache={onClearCache}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -134,6 +147,7 @@ PreviewPane.propTypes = {
|
|||
isCompiling: PropTypes.bool.isRequired,
|
||||
isDraftModeOn: PropTypes.bool.isRequired,
|
||||
isSyntaxCheckOn: PropTypes.bool.isRequired,
|
||||
isClearingCache: PropTypes.bool.isRequired,
|
||||
lastCompileTimestamp: PropTypes.number,
|
||||
logEntries: PropTypes.object,
|
||||
validationIssues: PropTypes.object,
|
||||
|
@ -144,14 +158,19 @@ PreviewPane.propTypes = {
|
|||
onClearCache: PropTypes.func.isRequired,
|
||||
onLogEntryLocationClick: PropTypes.func.isRequired,
|
||||
onRecompile: PropTypes.func.isRequired,
|
||||
onRecompileFromScratch: PropTypes.func.isRequired,
|
||||
onRunSyntaxCheckNow: PropTypes.func.isRequired,
|
||||
onSetAutoCompile: PropTypes.func.isRequired,
|
||||
onSetDraftMode: PropTypes.func.isRequired,
|
||||
onSetSyntaxCheck: PropTypes.func.isRequired,
|
||||
onSetSplitLayout: PropTypes.func.isRequired,
|
||||
onSetFullLayout: PropTypes.func.isRequired,
|
||||
onStopCompilation: PropTypes.func.isRequired,
|
||||
onToggleLogs: PropTypes.func.isRequired,
|
||||
outputFiles: PropTypes.array,
|
||||
pdfDownloadUrl: PropTypes.string,
|
||||
showLogs: PropTypes.bool.isRequired
|
||||
showLogs: PropTypes.bool.isRequired,
|
||||
splitLayout: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
export default PreviewPane
|
||||
|
|
|
@ -7,14 +7,14 @@ import Icon from '../../../shared/components/icon'
|
|||
function PreviewRecompileButton({
|
||||
compilerState: {
|
||||
isAutoCompileOn,
|
||||
isClearingCache,
|
||||
isCompiling,
|
||||
isDraftModeOn,
|
||||
isSyntaxCheckOn
|
||||
},
|
||||
onClearCache,
|
||||
onRecompile,
|
||||
onRecompileFromScratch,
|
||||
onRunSyntaxCheckNow,
|
||||
onStopCompilation,
|
||||
onSetAutoCompile,
|
||||
onSetDraftMode,
|
||||
onSetSyntaxCheck,
|
||||
|
@ -22,16 +22,6 @@ function PreviewRecompileButton({
|
|||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
function handleRecompileFromScratch() {
|
||||
onClearCache()
|
||||
.then(() => {
|
||||
onRecompile()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
function handleSelectAutoCompileOn() {
|
||||
onSetAutoCompile(true)
|
||||
}
|
||||
|
@ -69,9 +59,9 @@ function PreviewRecompileButton({
|
|||
}
|
||||
|
||||
if (!showText) {
|
||||
compilingProps = _hideText(isCompiling || isClearingCache)
|
||||
recompileProps = _hideText(!isCompiling || !isClearingCache)
|
||||
} else if (isCompiling || isClearingCache) {
|
||||
compilingProps = _hideText(isCompiling)
|
||||
recompileProps = _hideText(!isCompiling)
|
||||
} else if (isCompiling) {
|
||||
recompileProps = _hideText()
|
||||
} else {
|
||||
compilingProps = _hideText()
|
||||
|
@ -131,11 +121,20 @@ function PreviewRecompileButton({
|
|||
<Icon type="" modifier="fw" />
|
||||
{t('run_syntax_check_now')}
|
||||
</MenuItem>
|
||||
<MenuItem className={!isCompiling ? 'hidden' : ''} divider />
|
||||
<MenuItem
|
||||
onSelect={onStopCompilation}
|
||||
className={!isCompiling ? 'hidden' : ''}
|
||||
disabled={!isCompiling}
|
||||
aria-disabled={!isCompiling}
|
||||
>
|
||||
{t('stop_compile')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onSelect={handleRecompileFromScratch}
|
||||
disabled={isCompiling || isClearingCache}
|
||||
aria-disabled={!!(isCompiling || isClearingCache)}
|
||||
onSelect={onRecompileFromScratch}
|
||||
disabled={isCompiling}
|
||||
aria-disabled={!!isCompiling}
|
||||
>
|
||||
{t('recompile_from_scratch')}
|
||||
</MenuItem>
|
||||
|
@ -150,7 +149,7 @@ function PreviewRecompileButton({
|
|||
placement="bottom"
|
||||
overlay={
|
||||
<Tooltip id="tooltip-download-pdf">
|
||||
{isCompiling || isClearingCache ? t('compiling') : t('recompile')}
|
||||
{isCompiling ? t('compiling') : t('recompile')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
|
@ -162,18 +161,18 @@ function PreviewRecompileButton({
|
|||
PreviewRecompileButton.propTypes = {
|
||||
compilerState: PropTypes.shape({
|
||||
isAutoCompileOn: PropTypes.bool.isRequired,
|
||||
isClearingCache: PropTypes.bool.isRequired,
|
||||
isCompiling: PropTypes.bool.isRequired,
|
||||
isDraftModeOn: PropTypes.bool.isRequired,
|
||||
isSyntaxCheckOn: PropTypes.bool.isRequired,
|
||||
logEntries: PropTypes.object.isRequired
|
||||
}),
|
||||
onClearCache: PropTypes.func.isRequired,
|
||||
onRecompile: PropTypes.func.isRequired,
|
||||
onRecompileFromScratch: PropTypes.func.isRequired,
|
||||
onRunSyntaxCheckNow: PropTypes.func.isRequired,
|
||||
onSetAutoCompile: PropTypes.func.isRequired,
|
||||
onSetDraftMode: PropTypes.func.isRequired,
|
||||
onSetSyntaxCheck: PropTypes.func.isRequired,
|
||||
onStopCompilation: PropTypes.func.isRequired,
|
||||
showText: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewDownloadButton from './preview-download-button'
|
||||
import PreviewRecompileButton from './preview-recompile-button'
|
||||
import PreviewLogsToggleButton from './preview-logs-toggle-button'
|
||||
import useResizeObserver from '../../../shared/hooks/use-resize-observer'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
function _getElementWidth(element) {
|
||||
if (!element) return 0
|
||||
|
@ -13,16 +16,20 @@ function _getElementWidth(element) {
|
|||
function PreviewToolbar({
|
||||
compilerState,
|
||||
logsState,
|
||||
onClearCache,
|
||||
onRecompileFromScratch,
|
||||
onRecompile,
|
||||
onRunSyntaxCheckNow,
|
||||
onSetAutoCompile,
|
||||
onSetDraftMode,
|
||||
onSetSyntaxCheck,
|
||||
onToggleLogs,
|
||||
onSetSplitLayout,
|
||||
onSetFullLayout,
|
||||
onStopCompilation,
|
||||
outputFiles,
|
||||
pdfDownloadUrl,
|
||||
showLogs
|
||||
showLogs,
|
||||
splitLayout
|
||||
}) {
|
||||
const showTextRef = useRef(true)
|
||||
const showToggleTextRef = useRef(true)
|
||||
|
@ -33,6 +40,7 @@ function PreviewToolbar({
|
|||
const [showToggleText, setShowToggleText] = useState(
|
||||
showToggleTextRef.current
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
|
||||
function checkCanShowText(observedElement) {
|
||||
// toolbar items can be in 3 states:
|
||||
|
@ -182,6 +190,20 @@ function PreviewToolbar({
|
|||
return itemWidth
|
||||
}
|
||||
|
||||
const pdfExpandLabel = splitLayout ? t('full_screen') : t('split_screen')
|
||||
const pdfExpandIconType = splitLayout ? 'expand' : 'compress'
|
||||
const pdfExpandTooltip = (
|
||||
<Tooltip id="expand-pdf-btn">{pdfExpandLabel}</Tooltip>
|
||||
)
|
||||
|
||||
function handlePdfExpandBtnClick() {
|
||||
if (splitLayout) {
|
||||
onSetFullLayout()
|
||||
} else {
|
||||
onSetSplitLayout()
|
||||
}
|
||||
}
|
||||
|
||||
useResizeObserver(toolbarRef, logsState, checkCanShowText)
|
||||
|
||||
return (
|
||||
|
@ -195,11 +217,12 @@ function PreviewToolbar({
|
|||
<PreviewRecompileButton
|
||||
compilerState={compilerState}
|
||||
onRecompile={onRecompile}
|
||||
onRecompileFromScratch={onRecompileFromScratch}
|
||||
onRunSyntaxCheckNow={onRunSyntaxCheckNow}
|
||||
onSetAutoCompile={onSetAutoCompile}
|
||||
onSetDraftMode={onSetDraftMode}
|
||||
onSetSyntaxCheck={onSetSyntaxCheck}
|
||||
onClearCache={onClearCache}
|
||||
onStopCompilation={onStopCompilation}
|
||||
showText={showText}
|
||||
/>
|
||||
<PreviewDownloadButton
|
||||
|
@ -217,6 +240,15 @@ function PreviewToolbar({
|
|||
onToggle={onToggleLogs}
|
||||
showText={showToggleText}
|
||||
/>
|
||||
<OverlayTrigger placement="left" overlay={pdfExpandTooltip}>
|
||||
<button
|
||||
onClick={handlePdfExpandBtnClick}
|
||||
className="toolbar-pdf-expand-btn toolbar-item"
|
||||
aria-label={pdfExpandLabel}
|
||||
>
|
||||
<Icon type={pdfExpandIconType} />
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -237,13 +269,17 @@ PreviewToolbar.propTypes = {
|
|||
nLogEntries: PropTypes.number.isRequired
|
||||
}),
|
||||
showLogs: PropTypes.bool.isRequired,
|
||||
onClearCache: PropTypes.func.isRequired,
|
||||
splitLayout: PropTypes.bool.isRequired,
|
||||
onRecompile: PropTypes.func.isRequired,
|
||||
onRecompileFromScratch: PropTypes.func.isRequired,
|
||||
onRunSyntaxCheckNow: PropTypes.func.isRequired,
|
||||
onSetAutoCompile: PropTypes.func.isRequired,
|
||||
onSetDraftMode: PropTypes.func.isRequired,
|
||||
onSetSyntaxCheck: PropTypes.func.isRequired,
|
||||
onToggleLogs: PropTypes.func.isRequired,
|
||||
onSetSplitLayout: PropTypes.func.isRequired,
|
||||
onSetFullLayout: PropTypes.func.isRequired,
|
||||
onStopCompilation: PropTypes.func.isRequired,
|
||||
pdfDownloadUrl: PropTypes.string,
|
||||
outputFiles: PropTypes.array
|
||||
}
|
||||
|
|
|
@ -33,6 +33,14 @@ App.controller('PdfController', function(
|
|||
// view logic to check whether the files dropdown should "drop up" or "drop down"
|
||||
$scope.shouldDropUp = false
|
||||
|
||||
// Exposed methods for React layout handling
|
||||
$scope.setPdfSplitLayout = function() {
|
||||
$scope.$applyAsync(() => $scope.switchToSideBySideLayout('editor'))
|
||||
}
|
||||
$scope.setPdfFullLayout = function() {
|
||||
$scope.$applyAsync(() => $scope.switchToFlatLayout('pdf'))
|
||||
}
|
||||
|
||||
const logsContainerEl = document.querySelector('.pdf-logs')
|
||||
const filesDropdownEl =
|
||||
logsContainerEl && logsContainerEl.querySelector('.files-dropdown')
|
||||
|
@ -587,17 +595,20 @@ App.controller('PdfController', function(
|
|||
const logEntries = {
|
||||
all: [],
|
||||
errors: [],
|
||||
warnings: []
|
||||
warnings: [],
|
||||
typesetting: []
|
||||
}
|
||||
|
||||
function accumulateResults(newEntries) {
|
||||
for (let key of ['all', 'errors', 'warnings']) {
|
||||
if (newEntries.type != null) {
|
||||
for (let entry of newEntries[key]) {
|
||||
entry.type = newEntries.type
|
||||
for (let key of ['all', 'errors', 'warnings', 'typesetting']) {
|
||||
if (newEntries[key]) {
|
||||
if (newEntries.type != null) {
|
||||
for (let entry of newEntries[key]) {
|
||||
entry.type = newEntries.type
|
||||
}
|
||||
}
|
||||
logEntries[key] = logEntries[key].concat(newEntries[key])
|
||||
}
|
||||
logEntries[key] = logEntries[key].concat(newEntries[key])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -608,7 +619,7 @@ App.controller('PdfController', function(
|
|||
ignoreDuplicates: true
|
||||
})
|
||||
const all = [].concat(errors, warnings, typesetting)
|
||||
accumulateResults({ all, errors, warnings })
|
||||
accumulateResults({ all, errors, warnings, typesetting })
|
||||
}
|
||||
|
||||
function processChkTex(log) {
|
||||
|
@ -861,6 +872,19 @@ App.controller('PdfController', function(
|
|||
return deferred.promise
|
||||
}
|
||||
|
||||
$scope.recompileFromScratch = function() {
|
||||
$scope.pdf.compiling = true
|
||||
return $scope
|
||||
.clearCache()
|
||||
.then(() => {
|
||||
$scope.pdf.compiling = false
|
||||
$scope.recompile()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.toggleLogs = function() {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.shouldShowLogs = !$scope.shouldShowLogs
|
||||
|
|
|
@ -9,19 +9,30 @@ function useExpandCollapse({
|
|||
} = {}) {
|
||||
const ref = useRef()
|
||||
const [isExpanded, setIsExpanded] = useState(initiallyExpanded)
|
||||
const [size, setSize] = useState()
|
||||
const [sizing, setSizing] = useState({
|
||||
size: null,
|
||||
needsExpandCollapse: null
|
||||
})
|
||||
|
||||
useLayoutEffect(
|
||||
() => {
|
||||
const expandCollapseEl = ref.current
|
||||
if (isExpanded) {
|
||||
if (expandCollapseEl) {
|
||||
const expandedSize =
|
||||
dimension === 'height'
|
||||
? expandCollapseEl.scrollHeight
|
||||
: expandCollapseEl.scrollWidth
|
||||
setSize(expandedSize)
|
||||
} else {
|
||||
setSize(collapsedSize)
|
||||
|
||||
const needsExpandCollapse = expandedSize > collapsedSize
|
||||
|
||||
if (isExpanded) {
|
||||
setSizing({ size: expandedSize, needsExpandCollapse })
|
||||
} else {
|
||||
setSizing({
|
||||
size: needsExpandCollapse ? collapsedSize : expandedSize,
|
||||
needsExpandCollapse
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[isExpanded]
|
||||
|
@ -39,10 +50,11 @@ function useExpandCollapse({
|
|||
|
||||
return {
|
||||
isExpanded,
|
||||
needsExpandCollapse: sizing.needsExpandCollapse,
|
||||
expandableProps: {
|
||||
ref,
|
||||
style: {
|
||||
[dimension === 'height' ? 'height' : 'width']: `${size}px`
|
||||
[dimension === 'height' ? 'height' : 'width']: `${sizing.size}px`
|
||||
},
|
||||
className: expandableClasses
|
||||
},
|
||||
|
|
|
@ -17,7 +17,6 @@ function useResizeObserver(observedElement, observedData, callback) {
|
|||
() => {
|
||||
if ('ResizeObserver' in window) {
|
||||
const observedCurrent = observedElement && observedElement.current
|
||||
|
||||
if (observedCurrent) {
|
||||
observe(observedElement.current)
|
||||
}
|
||||
|
@ -27,7 +26,9 @@ function useResizeObserver(observedElement, observedData, callback) {
|
|||
}
|
||||
|
||||
return () => {
|
||||
unobserve(observedCurrent)
|
||||
if (observedCurrent) {
|
||||
unobserve(observedCurrent)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,42 +1,81 @@
|
|||
.logs-pane {
|
||||
.full-size;
|
||||
top: @pdf-top-offset;
|
||||
padding: @padding-sm;
|
||||
overflow-y: auto;
|
||||
background-color: @logs-pane-bg;
|
||||
}
|
||||
|
||||
.logs-pane-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: @padding-sm;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.logs-pane-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: @padding-sm 0;
|
||||
flex-grow: 1;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.logs-pane-actions-clear-cache {
|
||||
.no-outline-ring-on-click;
|
||||
margin-right: @margin-sm;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: @margin-sm;
|
||||
border-radius: @border-radius-base;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-entry-first-error-popup {
|
||||
border-radius: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.log-entry-header {
|
||||
padding: 3px @padding-sm;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: #fff;
|
||||
border-radius: @border-radius-base @border-radius-base 0 0;
|
||||
&:last-child {
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.log-entry-header-error {
|
||||
background-color: @ol-red;
|
||||
}
|
||||
|
||||
.log-entry-header-link-error {
|
||||
.btn-alert-variant(@ol-red);
|
||||
}
|
||||
|
||||
.log-entry-header-warning {
|
||||
background-color: @orange;
|
||||
}
|
||||
|
||||
.log-entry-header-link-warning {
|
||||
.btn-alert-variant(@orange);
|
||||
}
|
||||
|
||||
.log-entry-header-typesetting {
|
||||
background-color: @ol-blue;
|
||||
}
|
||||
|
||||
.log-entry-header-link-typesetting {
|
||||
.btn-alert-variant(@ol-blue);
|
||||
}
|
||||
|
||||
.log-entry-header-raw {
|
||||
background-color: @ol-blue-gray-4;
|
||||
}
|
||||
|
||||
.log-entry-header-link-raw {
|
||||
.btn-alert-variant(@ol-blue-gray-4);
|
||||
}
|
||||
|
||||
.log-entry-header-title,
|
||||
.log-entry-header-link {
|
||||
font-family: @font-family-sans-serif;
|
||||
|
@ -49,11 +88,14 @@
|
|||
}
|
||||
|
||||
.log-entry-header-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 9999px;
|
||||
border-width: 0;
|
||||
flex-grow: 0;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
padding-left: @padding-sm;
|
||||
margin-left: @margin-sm;
|
||||
max-width: 33%;
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
|
@ -64,59 +106,44 @@
|
|||
}
|
||||
}
|
||||
|
||||
.log-entry-header-link-location {
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding: 0 @padding-xs;
|
||||
}
|
||||
|
||||
.log-entry-content {
|
||||
background-color: #fff;
|
||||
padding: @padding-sm;
|
||||
&:last-child {
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-content-raw-expandable-container {
|
||||
position: relative;
|
||||
.log-entry-content-raw-container {
|
||||
background-color: @ol-blue-gray-1;
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
|
||||
.log-entry-content-raw-button-container-collapsed {
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-entry-content-raw {
|
||||
font-size: @font-size-small;
|
||||
font-size: @font-size-extra-small;
|
||||
color: @ol-blue-gray-4;
|
||||
padding: @padding-sm @padding-sm @padding-xl @padding-sm;
|
||||
padding: @padding-sm;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.log-entry-content-raw-collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.log-entry-content-button-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-top: 0;
|
||||
transition: margin 0.15s ease-in-out, opacity 0.15s ease-in-out;
|
||||
padding-bottom: @padding-sm;
|
||||
text-align: center;
|
||||
background-image: linear-gradient(0, @ol-blue-gray-1, transparent);
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-image: linear-gradient(0, @ol-blue-gray-1, transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-content-button-container-collapsed::before {
|
||||
opacity: 1;
|
||||
.log-entry-content-button-container-collapsed {
|
||||
margin-top: -40px;
|
||||
}
|
||||
|
||||
.log-entry-btn-expand-collapse {
|
||||
|
@ -139,7 +166,6 @@
|
|||
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);
|
||||
|
@ -149,7 +175,7 @@
|
|||
content: '';
|
||||
.triangle(top, @padding-sm, @padding-xs, @ol-red);
|
||||
top: -@padding-xs;
|
||||
right: @padding-md;
|
||||
right: @padding-xl;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,9 +183,20 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 @padding-sm @padding-sm @padding-sm;
|
||||
margin-top: -@margin-sm;
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
}
|
||||
|
||||
.first-error-btn {
|
||||
.no-outline-ring-on-click;
|
||||
}
|
||||
|
||||
.log-location-tooltip {
|
||||
word-break: break-all;
|
||||
&.tooltip.in {
|
||||
opacity: 1;
|
||||
}
|
||||
& > .tooltip-inner {
|
||||
max-width: 450px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -465,12 +465,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
#download-dropdown-list {
|
||||
max-height: calc(
|
||||
~'100vh - ' @toolbar-small-height ~' - ' @toolbar-height ~' - ' @margin-md
|
||||
);
|
||||
@editor-and-logs-pane-toolbars-height: @toolbar-small-height + @toolbar-height;
|
||||
@btn-small-height: (@padding-small-vertical * 2)+ (@font-size-small *
|
||||
@line-height-small);
|
||||
|
||||
#download-dropdown-list,
|
||||
#dropdown-files-logs-pane-list {
|
||||
overflow-y: auto;
|
||||
.dropdown-header {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
#download-dropdown-list {
|
||||
max-height: calc(
|
||||
~'100vh - ' @editor-and-logs-pane-toolbars-height ~' - ' @margin-md
|
||||
);
|
||||
}
|
||||
#dropdown-files-logs-pane-list {
|
||||
max-height: calc(
|
||||
~'100vh - ' @editor-and-logs-pane-toolbars-height ~' - ' @btn-small-height ~' - '
|
||||
@margin-md
|
||||
);
|
||||
}
|
||||
|
||||
.toolbar-pdf-expand-btn {
|
||||
.btn-inline-link;
|
||||
margin-left: @margin-xs;
|
||||
color: @toolbar-icon-btn-color;
|
||||
border-radius: @border-radius-small;
|
||||
&:hover {
|
||||
color: @toolbar-icon-btn-hover-color;
|
||||
}
|
||||
&:active {
|
||||
background-color: @link-color;
|
||||
color: #fff;
|
||||
}
|
||||
&:focus {
|
||||
outline: 0;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,19 +58,43 @@
|
|||
.alert-success {
|
||||
.alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);
|
||||
}
|
||||
|
||||
.btn-alert-success {
|
||||
.btn-alert-variant(@alert-success-bg);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
.alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);
|
||||
}
|
||||
|
||||
.btn-alert-info {
|
||||
.btn-alert-variant(@alert-info-bg);
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
.alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);
|
||||
}
|
||||
|
||||
.btn-alert-warning {
|
||||
.btn-alert-variant(@alert-warning-bg);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
.alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);
|
||||
}
|
||||
|
||||
.btn-alert-danger {
|
||||
.btn-alert-variant(@alert-danger-bg);
|
||||
}
|
||||
|
||||
.alert-alt {
|
||||
.alert-variant(@alert-alt-bg; @alert-alt-border; @alert-alt-text);
|
||||
}
|
||||
|
||||
.btn-alert-alt {
|
||||
.btn-alert-variant(@alert-alt-bg);
|
||||
}
|
||||
|
||||
.alert {
|
||||
a,
|
||||
.btn-inline-link {
|
||||
|
|
|
@ -522,7 +522,7 @@
|
|||
display: inline-block;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
.button-variant(#fff, shade(@background, 20%), transparent);
|
||||
.btn-alert-variant(@background);
|
||||
.button-size(
|
||||
@padding-xs-vertical; @padding-small-horizontal; @font-size-small;
|
||||
@line-height-small; @btn-border-radius-small
|
||||
|
@ -538,6 +538,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.btn-alert-variant(@alert-bg) {
|
||||
.button-variant(#fff, shade(@alert-bg, 15%), transparent);
|
||||
border-radius: @btn-border-radius-base;
|
||||
}
|
||||
|
||||
// Tables
|
||||
// -------------------------
|
||||
.table-row-variant(@state; @background) {
|
||||
|
|
|
@ -85,7 +85,8 @@
|
|||
|
||||
@font-size-base: 16px;
|
||||
@font-size-large: ceil((@font-size-base * 1.25)); // ~18px
|
||||
@font-size-small: ceil((@font-size-base * 0.85)); // ~12px
|
||||
@font-size-small: ceil((@font-size-base * 0.85)); // ~14px
|
||||
@font-size-extra-small: ceil((@font-size-base * 0.7)); // ~12px
|
||||
|
||||
@font-size-h1: floor((@font-size-base * 2)); // ~36px
|
||||
@font-size-h2: floor((@font-size-base * 1.6)); // ~30px
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"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",
|
||||
"compile_error_description": "This project did not compile because of an error",
|
||||
"validation_issue_description": "This project did not compile because of a validation issue",
|
||||
"compile_error_entry_description": "An error which prevented this project from compiling",
|
||||
"validation_issue_entry_description": "A validation issue which prevented this 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.",
|
||||
"first_error_popup_label": "This 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",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"n_errors_plural": "__count__ errors",
|
||||
"toggle_compile_options_menu": "Toggle compile options menu",
|
||||
"view_pdf": "View PDF",
|
||||
"your_project_has_errors": "Your project has errors",
|
||||
"your_project_has_errors": "This project has errors",
|
||||
"view_warnings": "View warnings",
|
||||
"view_logs": "View logs",
|
||||
"recompile_from_scratch": "Recompile from scratch",
|
||||
|
@ -913,7 +913,7 @@
|
|||
"no_errors_good_job": "No errors, good job!",
|
||||
"compile_error": "Compile Error",
|
||||
"generic_failed_compile_message": "Sorry, your LaTeX code couldn't compile for some reason. Please check the errors below for details, or view the raw log",
|
||||
"other_logs_and_files": "Other logs & files",
|
||||
"other_logs_and_files": "Other logs and files",
|
||||
"view_raw_logs": "View Raw Logs",
|
||||
"hide_raw_logs": "Hide Raw Logs",
|
||||
"clear_cache": "Clear cache",
|
||||
|
|
|
@ -2,23 +2,12 @@ import React from 'react'
|
|||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
|
||||
import PreviewDownloadButton, {
|
||||
topFileTypes
|
||||
} from '../../../../../frontend/js/features/preview/components/preview-download-button'
|
||||
import PreviewDownloadButton from '../../../../../frontend/js/features/preview/components/preview-download-button'
|
||||
|
||||
describe('<PreviewDownloadButton />', function() {
|
||||
const projectId = 'projectId123'
|
||||
const pdfDownloadUrl = `/download/project/${projectId}/build/17523aaafdf-1ad9063af140f004/output/output.pdf?compileGroup=priority&popupDownload=true`
|
||||
|
||||
function makeFile(fileName, main) {
|
||||
return {
|
||||
fileName,
|
||||
url: `/project/${projectId}/output/${fileName}`,
|
||||
type: fileName.split('.').pop(),
|
||||
main: main || false
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreviewDownloadButton(
|
||||
isCompiling,
|
||||
outputFiles,
|
||||
|
@ -92,72 +81,7 @@ describe('<PreviewDownloadButton />', function() {
|
|||
expect(buttons[0]).to.exist
|
||||
expect(buttons[0].getAttribute('disabled')).to.not.exist
|
||||
})
|
||||
it('should list all output files and group them', function() {
|
||||
const isCompiling = false
|
||||
const outputFiles = [
|
||||
makeFile('output.ind'),
|
||||
makeFile('output.log'),
|
||||
makeFile('output.pdf', true),
|
||||
makeFile('alt.pdf'),
|
||||
makeFile('output.stderr'),
|
||||
makeFile('output.stdout'),
|
||||
makeFile('output.aux'),
|
||||
makeFile('output.bbl'),
|
||||
makeFile('output.blg')
|
||||
]
|
||||
|
||||
renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
|
||||
|
||||
const menuItems = screen.getAllByRole('menuitem')
|
||||
expect(menuItems.length).to.equal(outputFiles.length - 1) // main PDF is listed separately
|
||||
|
||||
const fileTypes = outputFiles.map(file => {
|
||||
return file.type
|
||||
})
|
||||
menuItems.forEach((item, index) => {
|
||||
// check displayed text
|
||||
const fileType = item.textContent.split('.').pop()
|
||||
expect(fileTypes).to.include(fileType)
|
||||
})
|
||||
|
||||
// check grouped correctly
|
||||
expect(topFileTypes).to.exist
|
||||
expect(topFileTypes.length).to.be.above(0)
|
||||
const outputTopFileTypes = outputFiles
|
||||
.filter(file => {
|
||||
if (topFileTypes.includes(file.type)) return file.type
|
||||
})
|
||||
.map(file => file.type)
|
||||
const topMenuItems = menuItems.slice(0, outputTopFileTypes.length)
|
||||
topMenuItems.forEach(item => {
|
||||
const fileType = item.textContent
|
||||
.split('.')
|
||||
.pop()
|
||||
.replace(' file', '')
|
||||
expect(topFileTypes.includes(fileType)).to.be.true
|
||||
})
|
||||
})
|
||||
it('should list all files when there are duplicate types', function() {
|
||||
const isCompiling = false
|
||||
const pdfFile = makeFile('output.pdf', true)
|
||||
const bblFile = makeFile('output.bbl')
|
||||
const outputFiles = [Object.assign({}, { ...bblFile }), bblFile, pdfFile]
|
||||
|
||||
renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
|
||||
|
||||
const bblMenuItems = screen.getAllByText((content, element) => {
|
||||
return content !== '' && element.textContent === 'output.bbl'
|
||||
})
|
||||
expect(bblMenuItems.length).to.equal(2)
|
||||
})
|
||||
it('should list the non-main PDF in the dropdown', function() {
|
||||
const isCompiling = false
|
||||
const pdfFile = makeFile('output.pdf', true)
|
||||
const pdfAltFile = makeFile('alt.pdf')
|
||||
const outputFiles = [pdfFile, pdfAltFile]
|
||||
renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
|
||||
screen.getAllByRole('menuitem', { name: 'alt.pdf' })
|
||||
})
|
||||
it('should show the button text when prop showText=true', function() {
|
||||
const isCompiling = false
|
||||
const showText = true
|
||||
|
@ -172,39 +96,4 @@ describe('<PreviewDownloadButton />', function() {
|
|||
'position: absolute; right: -100vw;'
|
||||
)
|
||||
})
|
||||
describe('list divider and header', function() {
|
||||
it('should display when there are top files and other files', function() {
|
||||
const outputFiles = [
|
||||
makeFile('output.bbl'),
|
||||
makeFile('output.ind'),
|
||||
makeFile('output.gls'),
|
||||
makeFile('output.log')
|
||||
]
|
||||
|
||||
renderPreviewDownloadButton(false, outputFiles, pdfDownloadUrl, true)
|
||||
|
||||
screen.getByText('Download other output files')
|
||||
screen.getByRole('separator')
|
||||
})
|
||||
it('should not display when there are top files and no other files', function() {
|
||||
const outputFiles = [
|
||||
makeFile('output.bbl'),
|
||||
makeFile('output.ind'),
|
||||
makeFile('output.gls')
|
||||
]
|
||||
|
||||
renderPreviewDownloadButton(false, outputFiles, pdfDownloadUrl, true)
|
||||
|
||||
expect(screen.queryByText('Other output files')).to.not.exist
|
||||
expect(screen.queryByRole('separator')).to.not.exist
|
||||
})
|
||||
it('should not display when there are other files and no top files', function() {
|
||||
const outputFiles = [makeFile('output.log')]
|
||||
|
||||
renderPreviewDownloadButton(false, outputFiles, pdfDownloadUrl, true)
|
||||
|
||||
expect(screen.queryByText('Other output files')).to.not.exist
|
||||
expect(screen.queryByRole('separator')).to.not.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import React from 'react'
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
|
||||
import PreviewDownloadFileList, {
|
||||
topFileTypes
|
||||
} from '../../../../../frontend/js/features/preview/components/preview-download-file-list'
|
||||
|
||||
describe('<PreviewDownloadFileList />', function() {
|
||||
const projectId = 'projectId123'
|
||||
|
||||
function makeFile(fileName, main) {
|
||||
return {
|
||||
fileName,
|
||||
url: `/project/${projectId}/output/${fileName}`,
|
||||
type: fileName.split('.').pop(),
|
||||
main: main || false
|
||||
}
|
||||
}
|
||||
|
||||
it('should list all output files and group them', function() {
|
||||
const outputFiles = [
|
||||
makeFile('output.ind'),
|
||||
makeFile('output.log'),
|
||||
makeFile('output.pdf', true),
|
||||
makeFile('alt.pdf'),
|
||||
makeFile('output.stderr'),
|
||||
makeFile('output.stdout'),
|
||||
makeFile('output.aux'),
|
||||
makeFile('output.bbl'),
|
||||
makeFile('output.blg')
|
||||
]
|
||||
|
||||
render(<PreviewDownloadFileList fileList={outputFiles} />)
|
||||
|
||||
const menuItems = screen.getAllByRole('menuitem')
|
||||
expect(menuItems.length).to.equal(outputFiles.length - 1) // main PDF is listed separately
|
||||
|
||||
const fileTypes = outputFiles.map(file => {
|
||||
return file.type
|
||||
})
|
||||
menuItems.forEach((item, index) => {
|
||||
// check displayed text
|
||||
const fileType = item.textContent.split('.').pop()
|
||||
expect(fileTypes).to.include(fileType)
|
||||
})
|
||||
|
||||
// check grouped correctly
|
||||
expect(topFileTypes).to.exist
|
||||
expect(topFileTypes.length).to.be.above(0)
|
||||
const outputTopFileTypes = outputFiles
|
||||
.filter(file => {
|
||||
if (topFileTypes.includes(file.type)) return file.type
|
||||
})
|
||||
.map(file => file.type)
|
||||
const topMenuItems = menuItems.slice(0, outputTopFileTypes.length)
|
||||
topMenuItems.forEach(item => {
|
||||
const fileType = item.textContent
|
||||
.split('.')
|
||||
.pop()
|
||||
.replace(' file', '')
|
||||
expect(topFileTypes.includes(fileType)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
it('should list all files when there are duplicate types', function() {
|
||||
const pdfFile = makeFile('output.pdf', true)
|
||||
const bblFile = makeFile('output.bbl')
|
||||
const outputFiles = [Object.assign({}, { ...bblFile }), bblFile, pdfFile]
|
||||
|
||||
render(<PreviewDownloadFileList fileList={outputFiles} />)
|
||||
|
||||
const bblMenuItems = screen.getAllByText((content, element) => {
|
||||
return content !== '' && element.textContent === 'output.bbl'
|
||||
})
|
||||
expect(bblMenuItems.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('should list the non-main PDF in the dropdown', function() {
|
||||
const pdfFile = makeFile('output.pdf', true)
|
||||
const pdfAltFile = makeFile('alt.pdf')
|
||||
const outputFiles = [pdfFile, pdfAltFile]
|
||||
render(<PreviewDownloadFileList fileList={outputFiles} />)
|
||||
screen.getAllByRole('menuitem', { name: 'alt.pdf' })
|
||||
})
|
||||
|
||||
describe('list divider and header', function() {
|
||||
it('should display when there are top files and other files', function() {
|
||||
const outputFiles = [
|
||||
makeFile('output.bbl'),
|
||||
makeFile('output.ind'),
|
||||
makeFile('output.gls'),
|
||||
makeFile('output.log')
|
||||
]
|
||||
|
||||
render(<PreviewDownloadFileList fileList={outputFiles} />)
|
||||
|
||||
screen.getByText('Download other output files')
|
||||
screen.getByRole('separator')
|
||||
})
|
||||
|
||||
it('should not display when there are top files and no other files', function() {
|
||||
const outputFiles = [
|
||||
makeFile('output.bbl'),
|
||||
makeFile('output.ind'),
|
||||
makeFile('output.gls')
|
||||
]
|
||||
|
||||
render(<PreviewDownloadFileList fileList={outputFiles} />)
|
||||
|
||||
expect(screen.queryByText('Other output files')).to.not.exist
|
||||
expect(screen.queryByRole('separator')).to.not.exist
|
||||
})
|
||||
|
||||
it('should not display when there are other files and no top files', function() {
|
||||
const outputFiles = [makeFile('output.log')]
|
||||
|
||||
render(<PreviewDownloadFileList fileList={outputFiles} />)
|
||||
|
||||
expect(screen.queryByText('Other output files')).to.not.exist
|
||||
expect(screen.queryByRole('separator')).to.not.exist
|
||||
})
|
||||
})
|
||||
})
|
|
@ -76,6 +76,42 @@ describe('<PreviewLogsPaneEntry />', function() {
|
|||
describe('logs pane entry raw contents', function() {
|
||||
const rawContent = 'foo bar latex error stuff baz'
|
||||
|
||||
// JSDom doesn't compute layout/sizing, so we need to simulate sizing for the elements
|
||||
// Here we are simulating that the content is bigger than the `collapsedSize`, so
|
||||
// the expand-collapse widget is used
|
||||
const originalScrollHeight = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetHeight'
|
||||
)
|
||||
const originalScrollWidth = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetWidth'
|
||||
)
|
||||
|
||||
beforeEach(function() {
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
|
||||
configurable: true,
|
||||
value: 500
|
||||
})
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
|
||||
configurable: true,
|
||||
value: 500
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'scrollHeight',
|
||||
originalScrollHeight
|
||||
)
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'scrollWidth',
|
||||
originalScrollWidth
|
||||
)
|
||||
})
|
||||
|
||||
it('renders collapsed contents by default', function() {
|
||||
render(<PreviewLogsPaneEntry rawContent={rawContent} level={level} />)
|
||||
screen.getByText(rawContent)
|
||||
|
|
|
@ -49,65 +49,71 @@ entering extended mode
|
|||
const errors = [sampleError1, sampleError2]
|
||||
const warnings = [sampleWarning]
|
||||
const typesetting = [sampleTypesettingIssue]
|
||||
const logEntries = [...errors, ...warnings, ...typesetting]
|
||||
const logEntries = {
|
||||
all: [...errors, ...warnings, ...typesetting],
|
||||
errors,
|
||||
warnings,
|
||||
typesetting
|
||||
}
|
||||
|
||||
const onLogEntryLocationClick = sinon.stub()
|
||||
const noOp = () =>
|
||||
describe('with logs', function() {
|
||||
beforeEach(function() {
|
||||
render(
|
||||
<PreviewLogsPane
|
||||
logEntries={logEntries}
|
||||
rawLog={sampleRawLog}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
onClearCache={noOp}
|
||||
/>
|
||||
)
|
||||
})
|
||||
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)
|
||||
})
|
||||
|
||||
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.all.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 = {
|
||||
|
@ -125,10 +131,11 @@ entering extended mode
|
|||
<PreviewLogsPane
|
||||
validationIssues={sampleValidationIssues}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
onClearCache={noOp}
|
||||
/>
|
||||
)
|
||||
const validationEntries = screen.getAllByLabelText(
|
||||
'A validation issue which prevented your project from compiling'
|
||||
'A validation issue which prevented this project from compiling'
|
||||
)
|
||||
expect(validationEntries).to.have.lengthOf(
|
||||
Object.keys(sampleValidationIssues).length
|
||||
|
@ -140,10 +147,11 @@ entering extended mode
|
|||
<PreviewLogsPane
|
||||
validationIssues={{ unknownIssue: true }}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
onClearCache={noOp}
|
||||
/>
|
||||
)
|
||||
const validationEntries = screen.queryAllByLabelText(
|
||||
'A validation issue prevented your project from compiling'
|
||||
'A validation issue prevented this project from compiling'
|
||||
)
|
||||
expect(validationEntries).to.have.lengthOf(0)
|
||||
})
|
||||
|
@ -161,10 +169,11 @@ entering extended mode
|
|||
<PreviewLogsPane
|
||||
errors={sampleErrors}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
onClearCache={noOp}
|
||||
/>
|
||||
)
|
||||
const errorEntries = screen.getAllByLabelText(
|
||||
'An error which prevented your project from compiling'
|
||||
'An error which prevented this project from compiling'
|
||||
)
|
||||
expect(errorEntries).to.have.lengthOf(Object.keys(sampleErrors).length)
|
||||
})
|
||||
|
@ -174,10 +183,11 @@ entering extended mode
|
|||
<PreviewLogsPane
|
||||
errors={{ unknownIssue: true }}
|
||||
onLogEntryLocationClick={onLogEntryLocationClick}
|
||||
onClearCache={noOp}
|
||||
/>
|
||||
)
|
||||
const errorEntries = screen.queryAllByLabelText(
|
||||
'There was an error compiling your project'
|
||||
'There was an error compiling this project'
|
||||
)
|
||||
expect(errorEntries).to.have.lengthOf(0)
|
||||
})
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('<PreviewLogsToggleButton />', function() {
|
|||
nLogEntries: 0
|
||||
}
|
||||
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
|
||||
screen.getByText(`Your project has errors (${logsState.nErrors})`)
|
||||
screen.getByText(`This project has errors (${logsState.nErrors})`)
|
||||
})
|
||||
|
||||
it('should render an error status message when there are both errors and warnings', function() {
|
||||
|
@ -70,7 +70,7 @@ describe('<PreviewLogsToggleButton />', function() {
|
|||
nLogEntries: 0
|
||||
}
|
||||
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
|
||||
screen.getByText(`Your project has errors (${logsState.nErrors})`)
|
||||
screen.getByText(`This project has errors (${logsState.nErrors})`)
|
||||
})
|
||||
|
||||
it('should render a warning status message when there are warnings but no errors', function() {
|
||||
|
@ -90,7 +90,7 @@ describe('<PreviewLogsToggleButton />', function() {
|
|||
nLogEntries: 0
|
||||
}
|
||||
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
|
||||
screen.getByText('Your project has errors (9+)')
|
||||
screen.getByText('This project has errors (9+)')
|
||||
})
|
||||
it('should show the button text when prop showText=true', function() {
|
||||
const logsState = {
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('<PreviewPane />', function() {
|
|||
})
|
||||
render(<PreviewPane {...propsAfterCompileWithErrors} />)
|
||||
screen.getByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
screen.getByText(sampleError1.message)
|
||||
})
|
||||
|
@ -46,7 +46,7 @@ describe('<PreviewPane />', function() {
|
|||
render(<PreviewPane {...propsAfterCompileWithWarningsOnly} />)
|
||||
expect(
|
||||
screen.queryByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
@ -59,7 +59,7 @@ describe('<PreviewPane />', function() {
|
|||
render(<PreviewPane {...propsWhileCompiling} />)
|
||||
expect(
|
||||
screen.queryByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
@ -77,7 +77,7 @@ describe('<PreviewPane />', function() {
|
|||
render(<PreviewPane {...propsWithErrorsViewingLogs} />)
|
||||
expect(
|
||||
screen.queryByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
@ -108,7 +108,7 @@ describe('<PreviewPane />', function() {
|
|||
rerender(<PreviewPane {...propsWithErrorsAfterViewingLogs} />)
|
||||
expect(
|
||||
screen.queryByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
@ -139,7 +139,7 @@ describe('<PreviewPane />', function() {
|
|||
)
|
||||
rerender(<PreviewPane {...propsWithErrorsAfterSecondCompile} />)
|
||||
screen.getByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
screen.getByText(sampleError2.message)
|
||||
})
|
||||
|
@ -157,7 +157,7 @@ describe('<PreviewPane />', function() {
|
|||
fireEvent.click(dismissPopUpButton)
|
||||
expect(
|
||||
screen.queryByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
@ -191,7 +191,7 @@ describe('<PreviewPane />', function() {
|
|||
rerender(<PreviewPane {...propsWithErrorsForSecondCompile} />)
|
||||
expect(
|
||||
screen.queryByRole('alertdialog', {
|
||||
name: 'Your project has errors. This is the first one.'
|
||||
name: 'This project has errors. This is the first one.'
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
@ -228,7 +228,7 @@ describe('<PreviewPane />', function() {
|
|||
)
|
||||
render(<PreviewPane {...propsWithCLSIError} />)
|
||||
|
||||
screen.getByText('Your project did not compile because of an error')
|
||||
screen.getByText('This project did not compile because of an error')
|
||||
})
|
||||
|
||||
it('renders an accessible description for failed compiles with validation issues', function() {
|
||||
|
@ -248,7 +248,7 @@ describe('<PreviewPane />', function() {
|
|||
render(<PreviewPane {...propsWithValidationIssue} />)
|
||||
|
||||
screen.getByText(
|
||||
'Your project did not compile because of a validation issue'
|
||||
'This project did not compile because of a validation issue'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -262,6 +262,17 @@ describe('<PreviewPane />', function() {
|
|||
validationIssues = {},
|
||||
errors = {}
|
||||
) {
|
||||
const logEntriesWithDefaults = {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
typesetting: [],
|
||||
...logEntries
|
||||
}
|
||||
logEntriesWithDefaults.all = [
|
||||
...logEntriesWithDefaults.errors,
|
||||
...logEntriesWithDefaults.warnings,
|
||||
...logEntriesWithDefaults.typesetting
|
||||
]
|
||||
return {
|
||||
compilerState: {
|
||||
isAutoCompileOn: false,
|
||||
|
@ -270,7 +281,7 @@ describe('<PreviewPane />', function() {
|
|||
isDraftModeOn: false,
|
||||
isSyntaxCheckOn: false,
|
||||
lastCompileTimestamp,
|
||||
logEntries,
|
||||
logEntries: logEntriesWithDefaults,
|
||||
compileFailed,
|
||||
validationIssues,
|
||||
errors
|
||||
|
@ -278,12 +289,17 @@ describe('<PreviewPane />', function() {
|
|||
onClearCache: () => {},
|
||||
onLogEntryLocationClick: () => {},
|
||||
onRecompile: () => {},
|
||||
onRecompileFromScratch: () => {},
|
||||
onRunSyntaxCheckNow: () => {},
|
||||
onSetAutoCompile: () => {},
|
||||
onSetDraftMode: () => {},
|
||||
onSetSyntaxCheck: () => {},
|
||||
onToggleLogs: () => {},
|
||||
showLogs: isShowingLogs
|
||||
onSetSplitLayout: () => {},
|
||||
onSetFullLayout: () => {},
|
||||
onStopCompilation: () => {},
|
||||
showLogs: isShowingLogs,
|
||||
splitLayout: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,18 +5,19 @@ import PreviewRecompileButton from '../../../../../frontend/js/features/preview/
|
|||
const { expect } = require('chai')
|
||||
|
||||
describe('<PreviewRecompileButton />', function() {
|
||||
let onRecompile, onClearCache
|
||||
let onRecompile, onRecompileFromScratch, onStopCompilation
|
||||
|
||||
beforeEach(function() {
|
||||
onRecompile = sinon.stub().resolves()
|
||||
onClearCache = sinon.stub().resolves()
|
||||
onRecompileFromScratch = sinon.stub().resolves()
|
||||
onStopCompilation = sinon.stub().resolves()
|
||||
})
|
||||
|
||||
it('renders all items', function() {
|
||||
renderPreviewRecompileButton()
|
||||
|
||||
const menuItems = screen.getAllByRole('menuitem')
|
||||
expect(menuItems.length).to.equal(8)
|
||||
expect(menuItems.length).to.equal(9)
|
||||
expect(menuItems.map(item => item.textContent)).to.deep.equal([
|
||||
'On',
|
||||
'Off',
|
||||
|
@ -25,6 +26,7 @@ describe('<PreviewRecompileButton />', function() {
|
|||
'Check syntax before compile',
|
||||
"Don't check syntax",
|
||||
'Run syntax check now',
|
||||
'Stop compilation',
|
||||
'Recompile from scratch'
|
||||
])
|
||||
|
||||
|
@ -39,39 +41,17 @@ describe('<PreviewRecompileButton />', function() {
|
|||
|
||||
describe('Recompile from scratch', function() {
|
||||
describe('click', function() {
|
||||
it('should call onClearCache and onRecompile', async function() {
|
||||
it('should call onRecompileFromScratch', async function() {
|
||||
renderPreviewRecompileButton()
|
||||
|
||||
const button = screen.getByRole('menuitem', {
|
||||
name: 'Recompile from scratch'
|
||||
})
|
||||
await fireEvent.click(button)
|
||||
expect(onClearCache).to.have.been.calledOnce
|
||||
expect(onRecompile).to.have.been.calledOnce
|
||||
expect(onRecompileFromScratch).to.have.been.calledOnce
|
||||
})
|
||||
})
|
||||
describe('processing', function() {
|
||||
it('shows processing view and disable menuItem when clearing cache', function() {
|
||||
renderPreviewRecompileButton({ isClearingCache: true })
|
||||
|
||||
screen.getByRole('button', { name: 'Compiling …' })
|
||||
expect(
|
||||
screen
|
||||
.getByRole('menuitem', {
|
||||
name: 'Recompile from scratch'
|
||||
})
|
||||
.getAttribute('aria-disabled')
|
||||
).to.equal('true')
|
||||
expect(
|
||||
screen
|
||||
.getByRole('menuitem', {
|
||||
name: 'Recompile from scratch'
|
||||
})
|
||||
.closest('li')
|
||||
.getAttribute('class')
|
||||
).to.equal('disabled')
|
||||
})
|
||||
|
||||
it('shows processing view and disable menuItem when recompiling', function() {
|
||||
renderPreviewRecompileButton({ isCompiling: true })
|
||||
|
||||
|
@ -128,7 +108,8 @@ describe('<PreviewRecompileButton />', function() {
|
|||
onSetAutoCompile={() => {}}
|
||||
onSetDraftMode={() => {}}
|
||||
onSetSyntaxCheck={() => {}}
|
||||
onClearCache={onClearCache}
|
||||
onRecompileFromScratch={onRecompileFromScratch}
|
||||
onStopCompilation={onStopCompilation}
|
||||
showText={showText}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
import React from 'react'
|
||||
import sinon from 'sinon'
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
import { screen, render, fireEvent } from '@testing-library/react'
|
||||
import PreviewToolbar from '../../../../../frontend/js/features/preview/components/preview-toolbar'
|
||||
|
||||
describe('<PreviewToolbar />', function() {
|
||||
const onClearCache = sinon.stub()
|
||||
const onRecompile = sinon.stub()
|
||||
const onRecompileFromScratch = sinon.stub()
|
||||
const onRunSyntaxCheckNow = sinon.stub()
|
||||
const onSetAutoCompile = sinon.stub()
|
||||
const onSetDraftMode = sinon.stub()
|
||||
const onSetSyntaxCheck = sinon.stub()
|
||||
const onToggleLogs = sinon.stub()
|
||||
const onSetSplitLayout = sinon.stub()
|
||||
const onSetFullLayout = sinon.stub()
|
||||
const onStopCompilation = sinon.stub()
|
||||
|
||||
function renderPreviewToolbar(compilerState = {}, logState = {}, showLogs) {
|
||||
function renderPreviewToolbar(
|
||||
compilerState = {},
|
||||
logState = {},
|
||||
showLogs = false,
|
||||
splitLayout = true
|
||||
) {
|
||||
render(
|
||||
<PreviewToolbar
|
||||
compilerState={{
|
||||
|
@ -26,8 +34,8 @@ describe('<PreviewToolbar />', function() {
|
|||
...compilerState
|
||||
}}
|
||||
logsState={{ nErrors: 0, nWarnings: 0, nLogEntries: 0, ...logState }}
|
||||
onClearCache={onClearCache}
|
||||
onRecompile={onRecompile}
|
||||
onRecompileFromScratch={onRecompileFromScratch}
|
||||
onRunSyntaxCheckNow={onRunSyntaxCheckNow}
|
||||
onSetAutoCompile={onSetAutoCompile}
|
||||
onSetDraftMode={onSetDraftMode}
|
||||
|
@ -35,7 +43,11 @@ describe('<PreviewToolbar />', function() {
|
|||
onToggleLogs={onToggleLogs}
|
||||
outputFiles={[]}
|
||||
pdfDownloadUrl="/download-pdf-url"
|
||||
showLogs={showLogs || false}
|
||||
showLogs={showLogs}
|
||||
splitLayout={splitLayout}
|
||||
onSetSplitLayout={onSetSplitLayout}
|
||||
onSetFullLayout={onSetFullLayout}
|
||||
onStopCompilation={onStopCompilation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -63,4 +75,22 @@ describe('<PreviewToolbar />', function() {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('renders a full-screen button with a tooltip when when in split-screen mode', function() {
|
||||
renderPreviewToolbar()
|
||||
const btn = screen.getByLabelText('Full screen')
|
||||
fireEvent.click(btn)
|
||||
expect(onSetFullLayout).to.have.been.calledOnce
|
||||
fireEvent.mouseOver(btn)
|
||||
screen.getByRole('tooltip', { name: 'Full screen' })
|
||||
})
|
||||
|
||||
it('renders a split-screen button with a tooltip when when in full-screen mode', function() {
|
||||
renderPreviewToolbar({}, {}, false, false)
|
||||
const btn = screen.getByLabelText('Split screen')
|
||||
fireEvent.click(btn)
|
||||
expect(onSetSplitLayout).to.have.been.calledOnce
|
||||
fireEvent.mouseOver(btn)
|
||||
screen.getByRole('tooltip', { name: 'Split screen' })
|
||||
})
|
||||
})
|
||||
|
|
|
@ -14,6 +14,15 @@ const sampleContent = (
|
|||
</div>
|
||||
)
|
||||
|
||||
const originalScrollHeight = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetHeight'
|
||||
)
|
||||
const originalScrollWidth = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetWidth'
|
||||
)
|
||||
|
||||
function ExpandCollapseTestUI({ expandCollapseArgs }) {
|
||||
const { expandableProps } = useExpandCollapse(expandCollapseArgs)
|
||||
return (
|
||||
|
@ -27,6 +36,33 @@ ExpandCollapseTestUI.propTypes = {
|
|||
}
|
||||
|
||||
describe('useExpandCollapse', function() {
|
||||
// JSDom doesn't compute layout/sizing, so we need to simulate sizing for the elements
|
||||
// Here we are simulating that the content is bigger than the `collapsedSize`, so
|
||||
// the expand-collapse widget is used
|
||||
beforeEach(function() {
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
|
||||
configurable: true,
|
||||
value: 500
|
||||
})
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
|
||||
configurable: true,
|
||||
value: 500
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'scrollHeight',
|
||||
originalScrollHeight
|
||||
)
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'scrollWidth',
|
||||
originalScrollWidth
|
||||
)
|
||||
})
|
||||
|
||||
describe('custom CSS classes', function() {
|
||||
it('supports a custom CSS class', function() {
|
||||
const testArgs = {
|
||||
|
|
Loading…
Reference in a new issue