Add compile status indicator to new React-based errors UI.

GitOrigin-RevId: 545953e156d589a56ffd038bb7a40bba97770d06
This commit is contained in:
Paulo Reis 2020-09-29 12:08:49 +01:00 committed by Copybot
parent 9c7e9cf125
commit 876c292d22
12 changed files with 334 additions and 52 deletions

View file

@ -5,16 +5,16 @@ div.full-size.pdf(ng-controller="PdfController")
isAutoCompileOn: autocompile_enabled, isAutoCompileOn: autocompile_enabled,
isCompiling: pdf.compiling, isCompiling: pdf.compiling,
isDraftModeOn: draft, isDraftModeOn: draft,
isSyntaxCheckOn: stop_on_validation_error isSyntaxCheckOn: stop_on_validation_error,
logEntries: pdf.logEntries ? pdf.logEntries : {}
}` }`
log-entries="pdf.logEntries ? pdf.logEntries.all : []"
on-recompile="recompile" on-recompile="recompile"
on-run-syntax-check-now="runSyntaxCheckNow" on-run-syntax-check-now="runSyntaxCheckNow"
on-set-auto-compile="setAutoCompile" on-set-auto-compile="setAutoCompile"
on-set-draft-mode="setDraftMode" on-set-draft-mode="setDraftMode"
on-set-syntax-check="setSyntaxCheck" on-set-syntax-check="setSyntaxCheck"
on-toggle-logs="toggleLogs" on-toggle-logs="toggleLogs"
should-show-logs="shouldShowLogs" show-logs="shouldShowLogs"
) )
else else
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }") .toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")

View file

@ -21,5 +21,9 @@
"loading", "loading",
"no_messages", "no_messages",
"send_first_message", "send_first_message",
"your_message" "your_message",
"your_project_has_errors",
"view_warnings",
"view_logs",
"view_pdf"
] ]

View file

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classNames from 'classnames' import classNames from 'classnames'
import Icon from '../../../shared/components/icon'
function PreviewLogEntry({ file, line, message, content, raw, level }) { function PreviewLogEntry({ file, line, message, content, raw, level }) {
const logEntryClasses = classNames('alert', { const logEntryClasses = classNames('alert', {
@ -11,7 +12,7 @@ function PreviewLogEntry({ file, line, message, content, raw, level }) {
return ( return (
<div className={logEntryClasses}> <div className={logEntryClasses}>
<span className="line-no"> <span className="line-no">
<i className="fa fa-link" aria-hidden="true" /> <Icon type="link" />
{file ? <span>{file}</span> : null} {file ? <span>{file}</span> : null}
{line ? <span>, {line}</span> : null} {line ? <span>, {line}</span> : null}
</span> </span>

View file

@ -0,0 +1,110 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
function PreviewLogsToggleButton({
onToggle,
showLogs,
logsState: { nErrors, nWarnings }
}) {
const toggleButtonClasses = classNames('btn', 'btn-xs', 'btn-toggle-logs', {
'btn-danger': !showLogs && nErrors,
'btn-warning': !showLogs && !nErrors && nWarnings,
'btn-default': showLogs || (!nErrors && !nWarnings)
})
function handleOnClick(e) {
e.currentTarget.blur()
onToggle()
}
return (
<button
type="button"
className={toggleButtonClasses}
onClick={handleOnClick}
>
{showLogs ? (
<ViewPdfButton />
) : (
<CompilationResultIndicator nErrors={nErrors} nWarnings={nWarnings} />
)}
</button>
)
}
function CompilationResultIndicator({ nErrors, nWarnings }) {
if (nErrors) {
return <ErrorsCompilationResultIndicator nErrors={nErrors} />
} else if (nWarnings) {
return <WarningsCompilationResultIndicator nWarnings={nWarnings} />
} else {
return <ViewLogsButton />
}
}
function ErrorsCompilationResultIndicator({ nErrors }) {
const { t } = useTranslation()
return (
<>
<Icon type="file-text-o" />
<span className="btn-toggle-logs-label">
{`${t('your_project_has_errors')} (${nErrors > 9 ? '9+' : nErrors})`}
</span>
</>
)
}
function WarningsCompilationResultIndicator({ nWarnings }) {
const { t } = useTranslation()
return (
<>
<Icon type="file-text-o" />
<span className="btn-toggle-logs-label">
{`${t('view_warnings')} (${nWarnings > 9 ? '9+' : nWarnings})`}
</span>
</>
)
}
function ViewLogsButton() {
const { t } = useTranslation()
return (
<>
<Icon type="file-text-o" />
<span className="btn-toggle-logs-label">{t('view_logs')}</span>
</>
)
}
function ViewPdfButton() {
const { t } = useTranslation()
return (
<>
<Icon type="file-pdf-o" />
<span className="btn-toggle-logs-label">{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
}),
showLogs: PropTypes.bool.isRequired
}
ErrorsCompilationResultIndicator.propTypes = {
nErrors: PropTypes.number.isRequired
}
WarningsCompilationResultIndicator.propTypes = {
nWarnings: PropTypes.number.isRequired
}
export default PreviewLogsToggleButton

View file

@ -5,19 +5,33 @@ import PreviewLogsPane from './preview-logs-pane'
function PreviewPane({ function PreviewPane({
compilerState, compilerState,
logEntries,
onRecompile, onRecompile,
onRunSyntaxCheckNow, onRunSyntaxCheckNow,
onSetAutoCompile, onSetAutoCompile,
onSetDraftMode, onSetDraftMode,
onSetSyntaxCheck, onSetSyntaxCheck,
onToggleLogs, onToggleLogs,
shouldShowLogs showLogs
}) { }) {
const nErrors =
compilerState.logEntries && compilerState.logEntries.errors
? compilerState.logEntries.errors.length
: 0
const nWarnings =
compilerState.logEntries && compilerState.logEntries.warnings
? compilerState.logEntries.warnings.length
: 0
const nLogEntries =
compilerState.logEntries && compilerState.logEntries.all
? compilerState.logEntries.all.length
: 0
return ( return (
<> <>
<PreviewToolbar <PreviewToolbar
compilerState={compilerState} compilerState={compilerState}
logsState={{ nErrors, nWarnings, nLogEntries }}
showLogs={showLogs}
onRecompile={onRecompile} onRecompile={onRecompile}
onRunSyntaxCheckNow={onRunSyntaxCheckNow} onRunSyntaxCheckNow={onRunSyntaxCheckNow}
onSetAutoCompile={onSetAutoCompile} onSetAutoCompile={onSetAutoCompile}
@ -25,7 +39,9 @@ function PreviewPane({
onSetSyntaxCheck={onSetSyntaxCheck} onSetSyntaxCheck={onSetSyntaxCheck}
onToggleLogs={onToggleLogs} onToggleLogs={onToggleLogs}
/> />
{shouldShowLogs ? <PreviewLogsPane logEntries={logEntries} /> : null} {showLogs ? (
<PreviewLogsPane logEntries={compilerState.logEntries.all} />
) : null}
</> </>
) )
} }
@ -35,16 +51,16 @@ PreviewPane.propTypes = {
isAutoCompileOn: PropTypes.bool.isRequired, isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired, isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired, isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired isSyntaxCheckOn: PropTypes.bool.isRequired,
logEntries: PropTypes.object.isRequired
}), }),
logEntries: PropTypes.array,
onRecompile: PropTypes.func.isRequired, onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired, onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired, onSetAutoCompile: PropTypes.func.isRequired,
onSetDraftMode: PropTypes.func.isRequired, onSetDraftMode: PropTypes.func.isRequired,
onSetSyntaxCheck: PropTypes.func.isRequired, onSetSyntaxCheck: PropTypes.func.isRequired,
onToggleLogs: PropTypes.func.isRequired, onToggleLogs: PropTypes.func.isRequired,
shouldShowLogs: PropTypes.bool.isRequired showLogs: PropTypes.bool.isRequired
} }
export default PreviewPane export default PreviewPane

View file

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classNames from 'classnames'
import { Dropdown, MenuItem } from 'react-bootstrap' import { Dropdown, MenuItem } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
function PreviewRecompileButton({ function PreviewRecompileButton({
compilerState: { compilerState: {
@ -19,20 +19,6 @@ function PreviewRecompileButton({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const iconClasses = {
recompile: classNames('fa', 'fa-refresh', {
'fa-spin': isCompiling
}),
autoCompileOn: classNames('fa', 'fa-fw', { 'fa-check': isAutoCompileOn }),
autoCompileOff: classNames('fa', 'fa-fw', { 'fa-check': !isAutoCompileOn }),
compileModeNormal: classNames('fa', 'fa-fw', {
'fa-check': !isDraftModeOn
}),
compileModeDraft: classNames('fa', 'fa-fw', { 'fa-check': isDraftModeOn }),
syntaxCheckOn: classNames('fa', 'fa-fw', { 'fa-check': isSyntaxCheckOn }),
syntaxCheckOff: classNames('fa', 'fa-fw', { 'fa-check': !isSyntaxCheckOn })
}
function handleSelectAutoCompileOn() { function handleSelectAutoCompileOn() {
onSetAutoCompile(true) onSetAutoCompile(true)
} }
@ -60,7 +46,7 @@ function PreviewRecompileButton({
return ( return (
<Dropdown id="pdf-recompile-dropdown" className="btn-recompile-group"> <Dropdown id="pdf-recompile-dropdown" className="btn-recompile-group">
<button className="btn btn-recompile" onClick={onRecompile}> <button className="btn btn-recompile" onClick={onRecompile}>
<i className={iconClasses.recompile} aria-hidden="true" /> <Icon type="refresh" spin={isCompiling} />
{isCompiling ? ( {isCompiling ? (
<span className="btn-recompile-label"> <span className="btn-recompile-label">
{t('compiling')} {t('compiling')}
@ -74,33 +60,33 @@ function PreviewRecompileButton({
<Dropdown.Menu> <Dropdown.Menu>
<MenuItem header>{t('auto_compile')}</MenuItem> <MenuItem header>{t('auto_compile')}</MenuItem>
<MenuItem onSelect={handleSelectAutoCompileOn}> <MenuItem onSelect={handleSelectAutoCompileOn}>
<i className={iconClasses.autoCompileOn} aria-hidden="true" /> <Icon type={isAutoCompileOn ? 'check' : ''} modifier="fw" />
{t('on')} {t('on')}
</MenuItem> </MenuItem>
<MenuItem onSelect={handleSelectAutoCompileOff}> <MenuItem onSelect={handleSelectAutoCompileOff}>
<i className={iconClasses.autoCompileOff} aria-hidden="true" /> <Icon type={!isAutoCompileOn ? 'check' : ''} modifier="fw" />
{t('off')} {t('off')}
</MenuItem> </MenuItem>
<MenuItem header>{t('compile_mode')}</MenuItem> <MenuItem header>{t('compile_mode')}</MenuItem>
<MenuItem onSelect={handleSelectDraftModeOff}> <MenuItem onSelect={handleSelectDraftModeOff}>
<i className={iconClasses.compileModeNormal} aria-hidden="true" /> <Icon type={!isDraftModeOn ? 'check' : ''} modifier="fw" />
{t('normal')} {t('normal')}
</MenuItem> </MenuItem>
<MenuItem onSelect={handleSelectDraftModeOn}> <MenuItem onSelect={handleSelectDraftModeOn}>
<i className={iconClasses.compileModeDraft} aria-hidden="true" /> <Icon type={isDraftModeOn ? 'check' : ''} modifier="fw" />
{t('fast')} <span className="subdued">[draft]</span> {t('fast')} <span className="subdued">[draft]</span>
</MenuItem> </MenuItem>
<MenuItem header>Syntax Checks</MenuItem> <MenuItem header>Syntax Checks</MenuItem>
<MenuItem onSelect={handleSelectSyntaxCheckOn}> <MenuItem onSelect={handleSelectSyntaxCheckOn}>
<i className={iconClasses.syntaxCheckOn} aria-hidden="true" /> <Icon type={isSyntaxCheckOn ? 'check' : ''} modifier="fw" />
{t('stop_on_validation_error')} {t('stop_on_validation_error')}
</MenuItem> </MenuItem>
<MenuItem onSelect={handleSelectSyntaxCheckOff}> <MenuItem onSelect={handleSelectSyntaxCheckOff}>
<i className={iconClasses.syntaxCheckOff} aria-hidden="true" /> <Icon type={!isSyntaxCheckOn ? 'check' : ''} modifier="fw" />
{t('ignore_validation_errors')} {t('ignore_validation_errors')}
</MenuItem> </MenuItem>
<MenuItem onSelect={onRunSyntaxCheckNow}> <MenuItem onSelect={onRunSyntaxCheckNow}>
<i className="fa fa-fw" aria-hidden="true" /> <Icon type="" modifier="fw" />
{t('run_syntax_check_now')} {t('run_syntax_check_now')}
</MenuItem> </MenuItem>
</Dropdown.Menu> </Dropdown.Menu>
@ -113,7 +99,8 @@ PreviewRecompileButton.propTypes = {
isAutoCompileOn: PropTypes.bool.isRequired, isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired, isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired, isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired isSyntaxCheckOn: PropTypes.bool.isRequired,
logEntries: PropTypes.object.isRequired
}), }),
onRecompile: PropTypes.func.isRequired, onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired, onRunSyntaxCheckNow: PropTypes.func.isRequired,

View file

@ -1,29 +1,38 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import PreviewRecompileButton from './preview-recompile-button' import PreviewRecompileButton from './preview-recompile-button'
import PreviewLogsToggleButton from './preview-logs-toggle-button'
function PreviewToolbar({ function PreviewToolbar({
compilerState, compilerState,
logsState,
onRecompile, onRecompile,
onRunSyntaxCheckNow, onRunSyntaxCheckNow,
onSetAutoCompile, onSetAutoCompile,
onSetDraftMode, onSetDraftMode,
onSetSyntaxCheck, onSetSyntaxCheck,
onToggleLogs onToggleLogs,
showLogs
}) { }) {
return ( return (
<div className="toolbar toolbar-pdf"> <div className="toolbar toolbar-pdf">
<PreviewRecompileButton <div className="toolbar-pdf-left">
compilerState={compilerState} <PreviewRecompileButton
onRecompile={onRecompile} compilerState={compilerState}
onRunSyntaxCheckNow={onRunSyntaxCheckNow} onRecompile={onRecompile}
onSetAutoCompile={onSetAutoCompile} onRunSyntaxCheckNow={onRunSyntaxCheckNow}
onSetDraftMode={onSetDraftMode} onSetAutoCompile={onSetAutoCompile}
onSetSyntaxCheck={onSetSyntaxCheck} onSetDraftMode={onSetDraftMode}
/> onSetSyntaxCheck={onSetSyntaxCheck}
<button className="btn btn-sm btn-secondary" onClick={onToggleLogs}> />
Toggle logs </div>
</button> <div className="toolbar-pdf-right">
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
</div>
</div> </div>
) )
} }
@ -33,8 +42,15 @@ PreviewToolbar.propTypes = {
isAutoCompileOn: PropTypes.bool.isRequired, isAutoCompileOn: PropTypes.bool.isRequired,
isCompiling: PropTypes.bool.isRequired, isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired, isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired isSyntaxCheckOn: PropTypes.bool.isRequired,
logEntries: PropTypes.object.isRequired
}), }),
logsState: PropTypes.shape({
nErrors: PropTypes.number.isRequired,
nWarnings: PropTypes.number.isRequired,
nLogEntries: PropTypes.number.isRequired
}),
showLogs: PropTypes.bool.isRequired,
onRecompile: PropTypes.func.isRequired, onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired, onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired, onSetAutoCompile: PropTypes.func.isRequired,

View file

@ -25,7 +25,7 @@ export default (PdfManager = class PdfManager {
failure: false, // PDF failed to compile failure: false, // PDF failed to compile
compiling: false, compiling: false,
uncompiled: true, uncompiled: true,
logEntries: [], logEntries: {},
logEntryAnnotations: {}, logEntryAnnotations: {},
rawLog: '', rawLog: '',
view: null, // 'pdf' 'logs' view: null, // 'pdf' 'logs'

View file

@ -271,7 +271,7 @@ App.controller('PdfController', function(
} }
// if the previous run was a check, clear the error logs // if the previous run was a check, clear the error logs
if ($scope.check) { if ($scope.check) {
$scope.pdf.logEntries = [] $scope.pdf.logEntries = {}
} }
// keep track of whether this is a compile or check // keep track of whether this is a compile or check
$scope.check = !!options.check $scope.check = !!options.check
@ -622,7 +622,7 @@ App.controller('PdfController', function(
// output the results // output the results
function handleError() { function handleError() {
$scope.pdf.logEntries = [] $scope.pdf.logEntries = {}
$scope.pdf.rawLog = '' $scope.pdf.rawLog = ''
} }

View file

@ -33,6 +33,28 @@
border-bottom: 0; border-bottom: 0;
} }
.toolbar-pdf-left,
.toolbar-pdf-right {
display: flex;
align-items: center;
align-self: stretch;
flex: 1 1 100%;
}
.toolbar-pdf-right {
flex: 1 0 0px;
}
.btn-toggle-logs {
&:focus,
&:active:focus {
outline: none;
}
}
.btn-toggle-logs-label {
padding-left: @line-height-computed / 4;
}
.pdf { .pdf {
background-color: @pdf-bg; background-color: @pdf-bg;
} }

View file

@ -207,7 +207,7 @@
@btn-info-border: transparent; @btn-info-border: transparent;
@btn-warning-color: #fff; @btn-warning-color: #fff;
@btn-warning-bg: @ol-red; @btn-warning-bg: @orange;
@btn-warning-border: transparent; @btn-warning-border: transparent;
@btn-danger-color: #fff; @btn-danger-color: #fff;

View file

@ -0,0 +1,126 @@
import React from 'react'
import { screen, render } from '@testing-library/react'
import PreviewLogsToggleButton from '../../../../../frontend/js/features/preview/components/preview-logs-toggle-button'
describe('<PreviewLogsToggleButton />', function() {
describe('basic toggle functionality', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
}
const onToggleLogs = () => {}
it('should render a view logs button when previewing the PDF', function() {
const showLogs = false
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: 'View logs' })
})
it('should render a view PDF button when viewing logs', function() {
const showLogs = true
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: 'View PDF' })
})
})
describe('compile status indicator', function() {
const showLogs = false
const onToggleLogs = () => {}
it('should render a view logs button by default', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: 'View logs' })
})
it('should render an error status message when there are errors', function() {
const logsState = {
nErrors: 1,
nWarnings: 0,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', {
name: `Your project has errors (${logsState.nErrors})`
})
})
it('should render an error status message when there are both errors and warnings', function() {
const logsState = {
nErrors: 1,
nWarnings: 1,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', {
name: `Your project has errors (${logsState.nErrors})`
})
})
it('should render a warning status message when there are warnings but no errors', function() {
const logsState = {
nErrors: 0,
nWarnings: 1,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', {
name: `View warnings (${logsState.nWarnings})`
})
})
it('should render 9+ errors when there are more than nine errors', function() {
const logsState = {
nErrors: 10,
nWarnings: 0,
nLogEntries: 0
}
render(
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByRole('button', { name: `Your project has errors (9+)` })
})
})
})