1
0
Fork 0
mirror of https://github.com/overleaf/overleaf.git synced 2025-04-17 13:39:05 +00:00

New compile UI autocompile ()

* Animate recompile button when autocompile is waiting

* Add code-check failed notice to the new compile UI

GitOrigin-RevId: 83b62f41438e8e5b94bd893c222bec37745c0f57
This commit is contained in:
Paulo Jorge Reis 2021-03-18 13:49:01 +00:00 committed by Copybot
parent ba4300d9e1
commit 15f49994bd
13 changed files with 290 additions and 139 deletions

View file

@ -2,6 +2,8 @@ div.full-size.pdf(ng-controller="PdfController")
if showNewLogsUI
preview-pane(
compiler-state=`{
autoCompileHasChanges: changesToAutoCompile,
autoCompileHasLintingError: autoCompileLintingError,
isAutoCompileOn: autocompile_enabled,
isClearingCache: pdf.clearingCache,
isCompiling: pdf.compiling,

View file

@ -25,6 +25,8 @@
"close": "",
"clsi_maintenance": "",
"clsi_unavailable": "",
"code_check_failed": "",
"code_check_failed_explanation": "",
"collabs_per_proj": "",
"collapse": "",
"common": "",
@ -199,4 +201,4 @@
"zotero_reference_loading_error_expired": "",
"zotero_reference_loading_error_forbidden": "",
"zotero_sync_description": ""
}
}

View file

@ -17,6 +17,7 @@ function PreviewLogsPane({
outputFiles = [],
isClearingCache,
isCompiling = false,
autoCompileHasLintingError = false,
onLogEntryLocationClick,
onClearCache
}) {
@ -113,6 +114,7 @@ function PreviewLogsPane({
<div className="logs-pane">
<div className="logs-pane-content">
<LogsPaneBetaNotice />
{autoCompileHasLintingError ? <AutoCompileLintingErrorEntry /> : null}
{errors ? errorsUI : null}
{validationIssues ? validationIssuesUI : null}
{allCompilerIssues.length > 0 ? logEntriesUI : null}
@ -123,6 +125,22 @@ function PreviewLogsPane({
)
}
function AutoCompileLintingErrorEntry() {
const { t } = useTranslation()
return (
<div className="log-entry">
<div className="log-entry-header log-entry-header-error">
<div className="log-entry-header-icon-container">
<Icon type="exclamation-triangle" modifier="fw" />
</div>
<h3 className="log-entry-header-title">
{t('code_check_failed_explanation')}
</h3>
</div>
</div>
)
}
function LogsPaneBetaNotice() {
const { t } = useTranslation()
const [dismissedBetaNotice, setDismissedBetaNotice] = usePersistedState(
@ -173,6 +191,7 @@ PreviewLogsPane.propTypes = {
warning: PropTypes.array,
typesetting: PropTypes.array
}),
autoCompileHasLintingError: PropTypes.bool,
rawLog: PropTypes.string,
outputFiles: PropTypes.array,
isClearingCache: PropTypes.bool,

View file

@ -10,24 +10,16 @@ const MAX_ERRORS_COUNT = 99
function PreviewLogsToggleButton({
onToggle,
showLogs,
autoCompileLintingError = false,
compileFailed = false,
logsState: { nErrors, nWarnings },
showText
}) {
const { t } = useTranslation()
const toggleButtonClasses = classNames(
'btn',
'btn-xs',
'btn-toggle-logs',
'toolbar-item',
{
'btn-danger': !showLogs && nErrors,
'btn-warning': !showLogs && !nErrors && nWarnings,
'btn-default': showLogs || (!nErrors && !nWarnings)
}
)
let textStyle = {}
let btnColorCssClass = 'btn-default'
let buttonContents
if (!showText) {
textStyle = {
position: 'absolute',
@ -40,23 +32,40 @@ function PreviewLogsToggleButton({
onToggle()
}
if (showLogs) {
buttonContents = <ViewPdf textStyle={textStyle} />
} else {
buttonContents = (
<CompilationResult
textStyle={textStyle}
autoCompileLintingError={autoCompileLintingError}
nErrors={nErrors}
nWarnings={nWarnings}
/>
)
if (autoCompileLintingError || nErrors > 0) {
btnColorCssClass = 'btn-danger'
} else if (nWarnings > 0) {
btnColorCssClass = 'btn-warning'
}
}
const buttonClasses = classNames(
'btn',
'btn-xs',
'btn-toggle-logs',
'toolbar-item',
btnColorCssClass
)
const buttonElement = (
<button
id="logs-toggle"
type="button"
disabled={compileFailed}
className={toggleButtonClasses}
className={buttonClasses}
onClick={handleOnClick}
>
{showLogs ? (
<ViewPdfButton textStyle={textStyle} />
) : (
<CompilationResultIndicator
textStyle={textStyle}
nErrors={nErrors}
nWarnings={nWarnings}
/>
)}
{buttonContents}
</button>
)
@ -76,21 +85,40 @@ function PreviewLogsToggleButton({
)
}
function CompilationResultIndicator({ textStyle, nErrors, nWarnings }) {
if (nErrors || nWarnings) {
function CompilationResult({
textStyle,
autoCompileLintingError,
nErrors,
nWarnings
}) {
if (autoCompileLintingError) {
return <AutoCompileLintingError textStyle={textStyle} />
} else if (nErrors || nWarnings) {
return (
<LogsCompilationResultIndicator
<LogsCompilationResult
logType={nErrors ? 'errors' : 'warnings'}
nLogs={nErrors || nWarnings}
textStyle={textStyle}
/>
)
} else {
return <ViewLogsButton textStyle={textStyle} />
return <ViewLogs textStyle={textStyle} />
}
}
function LogsCompilationResultIndicator({ textStyle, logType, nLogs }) {
function ViewPdf({ textStyle }) {
const { t } = useTranslation()
return (
<>
<Icon type="file-pdf-o" />
<span className="toolbar-text" style={textStyle}>
{t('view_pdf')}
</span>
</>
)
}
function LogsCompilationResult({ textStyle, logType, nLogs }) {
const { t } = useTranslation()
const label =
logType === 'errors' ? t('your_project_has_errors') : t('view_warnings')
@ -110,7 +138,19 @@ function LogsCompilationResultIndicator({ textStyle, logType, nLogs }) {
)
}
function ViewLogsButton({ textStyle }) {
function AutoCompileLintingError({ textStyle }) {
const { t } = useTranslation()
return (
<>
<Icon type="exclamation-triangle" />
<span className="toolbar-text" style={textStyle}>
{t('code_check_failed')}
</span>
</>
)
}
function ViewLogs({ textStyle }) {
const { t } = useTranslation()
return (
<>
@ -122,41 +162,40 @@ function ViewLogsButton({ textStyle }) {
)
}
function ViewPdfButton({ textStyle }) {
const { t } = useTranslation()
return (
<>
<Icon type="file-pdf-o" />
<span className="toolbar-text" style={textStyle}>
{t('view_pdf')}
</span>
</>
)
}
PreviewLogsToggleButton.propTypes = {
onToggle: PropTypes.func.isRequired,
logsState: PropTypes.shape({
nErrors: PropTypes.number.isRequired,
nWarnings: PropTypes.number.isRequired,
nLogEntries: PropTypes.number.isRequired
nWarnings: PropTypes.number.isRequired
}),
showLogs: PropTypes.bool.isRequired,
showText: PropTypes.bool.isRequired,
compileFailed: PropTypes.bool
compileFailed: PropTypes.bool,
autoCompileLintingError: PropTypes.bool
}
LogsCompilationResultIndicator.propTypes = {
CompilationResult.propTypes = {
textStyle: PropTypes.object.isRequired,
autoCompileLintingError: PropTypes.bool,
nErrors: PropTypes.number.isRequired,
nWarnings: PropTypes.number.isRequired
}
LogsCompilationResult.propTypes = {
logType: PropTypes.string.isRequired,
nLogs: PropTypes.number.isRequired,
textStyle: PropTypes.object.isRequired
}
ViewLogsButton.propTypes = {
AutoCompileLintingError.propTypes = {
textStyle: PropTypes.object.isRequired
}
ViewPdfButton.propTypes = {
ViewLogs.propTypes = {
textStyle: PropTypes.object.isRequired
}
ViewPdf.propTypes = {
textStyle: PropTypes.object.isRequired
}

View file

@ -53,10 +53,6 @@ function PreviewPane({
compilerState.logEntries && compilerState.logEntries.warnings
? compilerState.logEntries.warnings.length
: 0
const nLogEntries =
compilerState.logEntries && compilerState.logEntries.all
? compilerState.logEntries.all.length
: 0
const hasCLSIErrors =
compilerState.errors &&
@ -84,7 +80,7 @@ function PreviewPane({
<>
<PreviewToolbar
compilerState={compilerState}
logsState={{ nErrors, nWarnings, nLogEntries }}
logsState={{ nErrors, nWarnings }}
showLogs={showLogs}
onRecompile={onRecompile}
onRecompileFromScratch={onRecompileFromScratch}
@ -130,6 +126,7 @@ function PreviewPane({
rawLog={compilerState.rawLog}
validationIssues={compilerState.validationIssues}
errors={compilerState.errors}
autoCompileHasLintingError={compilerState.autoCompileHasLintingError}
outputFiles={outputFiles}
onLogEntryLocationClick={onLogEntryLocationClick}
isClearingCache={compilerState.isClearingCache}
@ -143,6 +140,7 @@ function PreviewPane({
PreviewPane.propTypes = {
compilerState: PropTypes.shape({
autoCompileHasLintingError: PropTypes.bool,
isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,

View file

@ -2,10 +2,13 @@ import React from 'react'
import PropTypes from 'prop-types'
import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import Icon from '../../../shared/components/icon'
function PreviewRecompileButton({
compilerState: {
autoCompileHasChanges,
autoCompileHasLintingError,
isAutoCompileOn,
isCompiling,
isDraftModeOn,
@ -67,10 +70,19 @@ function PreviewRecompileButton({
compilingProps = _hideText()
}
const recompileButtonGroupClasses = classNames(
'btn-recompile-group',
'toolbar-item',
{
'btn-recompile-group-has-changes':
autoCompileHasChanges && !autoCompileHasLintingError
}
)
const buttonElement = (
<Dropdown
id="pdf-recompile-dropdown"
className="btn-recompile-group toolbar-item"
className={recompileButtonGroupClasses}
>
<button className="btn btn-recompile" onClick={onRecompile}>
<Icon type="refresh" spin={isCompiling} />
@ -160,6 +172,8 @@ function PreviewRecompileButton({
PreviewRecompileButton.propTypes = {
compilerState: PropTypes.shape({
autoCompileHasChanges: PropTypes.bool.isRequired,
autoCompileHasLintingError: PropTypes.bool,
isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,

View file

@ -236,6 +236,10 @@ function PreviewToolbar({
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
autoCompileLintingError={
compilerState.isAutoCompileOn &&
compilerState.autoCompileHasLintingError
}
compileFailed={compilerState.compileFailed}
onToggle={onToggleLogs}
showText={showToggleText}
@ -256,6 +260,7 @@ function PreviewToolbar({
PreviewToolbar.propTypes = {
compilerState: PropTypes.shape({
autoCompileHasLintingError: PropTypes.bool,
isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,
@ -265,8 +270,7 @@ PreviewToolbar.propTypes = {
}),
logsState: PropTypes.shape({
nErrors: PropTypes.number.isRequired,
nWarnings: PropTypes.number.isRequired,
nLogEntries: PropTypes.number.isRequired
nWarnings: PropTypes.number.isRequired
}),
showLogs: PropTypes.bool.isRequired,
splitLayout: PropTypes.bool.isRequired,

View file

@ -83,19 +83,29 @@
.btn-recompile-group {
align-self: stretch;
margin-right: 6px;
.btn-recompile {
height: 100%;
.btn-primary;
padding-top: 3px;
padding-bottom: 3px;
&:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&[disabled] {
background-color: mix(@btn-primary-bg, @toolbar-alt-bg-color, 65%);
.opacity(1);
}
border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0;
background-color: @btn-primary-bg;
&.btn-recompile-group-has-changes {
#gradient > .striped(@color: rgba(255, 255, 255, 0.2), @angle: -45deg);
background-size: @stripe-width @stripe-width;
.animation(pdf-toolbar-stripes 2s linear infinite);
}
}
.btn-recompile {
height: 100%;
// .btn-primary;
color: #fff;
background-color: transparent;
padding-top: 3px;
padding-bottom: 3px;
&:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&[disabled] {
background-color: mix(@btn-primary-bg, @toolbar-alt-bg-color, 65%);
.opacity(1);
}
}

View file

@ -56,63 +56,63 @@ entering extended mode
warnings,
typesetting
}
const noOp = () => {}
const onLogEntryLocationClick = sinon.stub()
const noOp = () =>
describe('with logs', function() {
beforeEach(function() {
renderWithEditorContext(
<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)
})
it('renders the raw log', function() {
screen.getByLabelText('Raw logs from the LaTeX compiler')
})
describe('with logs', function() {
beforeEach(function() {
renderWithEditorContext(
<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)
})
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}`
})
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('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.all.forEach((entry, index) => {
const linkToSourceButton = screen.getByRole('button', {
name: `Navigate to log position in source code: ${entry.file}, ${entry.line}`
})
})
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
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
})
})
describe('with validation issues', function() {
const sampleValidationIssues = {
@ -191,4 +191,23 @@ entering extended mode
expect(errorEntries).to.have.lengthOf(0)
})
})
describe('with failing code check', function() {
beforeEach(function() {
renderWithEditorContext(
<PreviewLogsPane
logEntries={logEntries}
rawLog={sampleRawLog}
onLogEntryLocationClick={onLogEntryLocationClick}
onClearCache={noOp}
autoCompileHasLintingError
/>
)
})
it('renders a code check failed entry', function() {
screen.getByText(
'Your code has errors that need to be fixed before the auto-compile can run'
)
})
})
})

View file

@ -9,15 +9,18 @@ describe('<PreviewLogsToggleButton />', function() {
logsState,
onToggleLogs,
showLogs,
showText
showText = false,
autoCompileLintingError = false,
compileFailed = false
) {
if (showText === undefined) showText = true
render(
<PreviewLogsToggleButton
logsState={logsState}
onToggle={onToggleLogs}
showLogs={showLogs}
showText={showText}
autoCompileLintingError={autoCompileLintingError}
compileFailed={compileFailed}
/>
)
}
@ -25,8 +28,7 @@ describe('<PreviewLogsToggleButton />', function() {
describe('basic toggle functionality', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
nWarnings: 0
}
const onToggleLogs = () => {}
it('should render a view logs button when previewing the PDF', function() {
@ -46,18 +48,31 @@ describe('<PreviewLogsToggleButton />', function() {
it('should render a view logs button by default', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
nWarnings: 0
}
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
screen.getByText('View logs')
})
it('should render the code check failed notice', function() {
const logsState = {
nErrors: 1,
nWarnings: 0
}
renderPreviewLogsToggleButton(
logsState,
onToggleLogs,
showLogs,
false,
true
)
screen.getByText('Code check failed')
})
it('should render an error status message when there are errors', function() {
const logsState = {
nErrors: 1,
nWarnings: 0,
nLogEntries: 0
nWarnings: 0
}
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
screen.getByText(`This project has errors (${logsState.nErrors})`)
@ -66,8 +81,7 @@ describe('<PreviewLogsToggleButton />', function() {
it('should render an error status message when there are both errors and warnings', function() {
const logsState = {
nErrors: 1,
nWarnings: 1,
nLogEntries: 0
nWarnings: 1
}
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
screen.getByText(`This project has errors (${logsState.nErrors})`)
@ -76,8 +90,7 @@ describe('<PreviewLogsToggleButton />', function() {
it('should render a warning status message when there are warnings but no errors', function() {
const logsState = {
nErrors: 0,
nWarnings: 1,
nLogEntries: 0
nWarnings: 1
}
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
screen.getByText(`View warnings (${logsState.nWarnings})`)
@ -86,8 +99,7 @@ describe('<PreviewLogsToggleButton />', function() {
it('should render 99+ errors when there are more than 99 errors', function() {
const logsState = {
nErrors: 100,
nWarnings: 0,
nLogEntries: 0
nWarnings: 0
}
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
screen.getByText('This project has errors (99+)')
@ -95,8 +107,7 @@ describe('<PreviewLogsToggleButton />', function() {
it('should show the button text when prop showText=true', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
nWarnings: 0
}
const showText = true
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs, showText)
@ -105,8 +116,7 @@ describe('<PreviewLogsToggleButton />', function() {
it('should not show the button text when prop showText=false', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
nWarnings: 0
}
const showText = false
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs, showText)

View file

@ -275,6 +275,7 @@ describe('<PreviewPane />', function() {
]
return {
compilerState: {
autoCompileHasChanges: false,
isAutoCompileOn: false,
isCompiling: isCompiling,
isClearingCache: false,

View file

@ -88,14 +88,46 @@ describe('<PreviewRecompileButton />', function() {
)
})
describe('Autocompile feedback', function() {
it('shows animated visual feedback via CSS class when there are uncompiled changes', function() {
const { container } = renderPreviewRecompileButton({
autoCompileHasChanges: true,
autoCompileHasLintingError: false
})
const recompileBtnGroupEl = container.querySelector(
'.btn-recompile-group'
)
expect(
recompileBtnGroupEl.classList.contains(
'btn-recompile-group-has-changes'
)
).to.be.true
})
it('does not show animated visual feedback via CSS class when there are no uncompiled changes', function() {
const { container } = renderPreviewRecompileButton({
autoCompileHasChanges: false,
autoCompileHasLintingError: false
})
const recompileBtnGroupEl = container.querySelector(
'.btn-recompile-group'
)
expect(
recompileBtnGroupEl.classList.contains(
'btn-recompile-group-has-changes'
)
).to.be.false
})
})
function renderPreviewRecompileButton(compilerState = {}, showText) {
if (!compilerState.logEntries) {
compilerState.logEntries = {}
}
if (showText === undefined) showText = true
render(
return render(
<PreviewRecompileButton
compilerState={{
autoCompileHasChanges: false,
isAutoCompileOn: true,
isClearingCache: false,
isCompiling: false,

View file

@ -25,6 +25,7 @@ describe('<PreviewToolbar />', function() {
render(
<PreviewToolbar
compilerState={{
autoCompileHasChanges: true,
isAutoCompileOn: true,
isClearingCache: false,
isCompiling: false,
@ -33,7 +34,7 @@ describe('<PreviewToolbar />', function() {
logEntries: {},
...compilerState
}}
logsState={{ nErrors: 0, nWarnings: 0, nLogEntries: 0, ...logState }}
logsState={{ nErrors: 0, nWarnings: 0, ...logState }}
onRecompile={onRecompile}
onRecompileFromScratch={onRecompileFromScratch}
onRunSyntaxCheckNow={onRunSyntaxCheckNow}