diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js b/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js
index 657ce4efd7..95f74a9aa8 100644
--- a/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js
+++ b/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js
@@ -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 (
-
+
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 ? (
+
+
+
+
+ {`\u202A${locationLinkText}\u202C`}
+
+
+ ) : null
+
+ const locationTooltip = showLocationTooltip ? (
+
+ {locationLinkText}
+
+ ) : null
+
return (
{headerTitle}
- {showSourceLocationLink && file ? (
-
-
-
- {file}
- {line ? , {line} : null}
-
- ) : null}
+ {showLocationTooltip ? (
+
+ {locationLink}
+
+ ) : (
+ locationLink
+ )}
{showCloseButton ? (
{rawContent ? (
-
-
{rawContent.trim()}
-
-
- {isExpanded ? (
- <>
- {t('collapse')}
- >
- ) : (
- <>
- {t('expand')}
- >
- )}
-
+
+
+ {needsExpandCollapse ? (
+
+
+ {isExpanded ? (
+ <>
+ {t('collapse')}
+ >
+ ) : (
+ <>
+ {t('expand')}
+ >
+ )}
+
+
+ ) : null}
) : 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,
diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane.js b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
index 6e99aa4c16..f532414d46 100644
--- a/services/web/frontend/js/features/preview/components/preview-logs-pane.js
+++ b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
@@ -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) => (
@@ -28,7 +41,11 @@ function PreviewLogsPane({
)
)
- const logEntriesUI = logEntries.map((logEntry, idx) => (
+ const logEntriesUI = [
+ ...compilerErrors,
+ ...compilerWarnings,
+ ...compilerTypesettingIssues
+ ].map((logEntry, idx) => (
))
+ const actionsUI = (
+
+
+ {isClearingCache ? (
+
+ ) : (
+
+ )}
+
+ {t('clear_cached_files')}
+
+
+
+
+
+
+
+
+ )
+
const rawLogUI = (
- {errors ? errorsUI : null}
- {validationIssues ? validationIssuesUI : null}
- {logEntries ? logEntriesUI : null}
- {rawLog && rawLog !== '' ? rawLogUI : null}
+
+ {errors ? errorsUI : null}
+ {validationIssues ? validationIssuesUI : null}
+ {allCompilerIssues.length > 0 ? logEntriesUI : null}
+ {rawLog && rawLog !== '' ? rawLogUI : null}
+ {actionsUI}
+
)
}
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
}
diff --git a/services/web/frontend/js/features/preview/components/preview-pane.js b/services/web/frontend/js/features/preview/components/preview-pane.js
index 3b80b2e265..b4622fba78 100644
--- a/services/web/frontend/js/features/preview/components/preview-pane.js
+++ b/services/web/frontend/js/features/preview/components/preview-pane.js
@@ -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}
/>
{hasCLSIErrors ? t('compile_error_description') : ''}
@@ -117,11 +126,15 @@ function PreviewPane({
) : null}
{showLogs ? (
) : 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
diff --git a/services/web/frontend/js/features/preview/components/preview-recompile-button.js b/services/web/frontend/js/features/preview/components/preview-recompile-button.js
index ad5d880bfc..f6fe1bde52 100644
--- a/services/web/frontend/js/features/preview/components/preview-recompile-button.js
+++ b/services/web/frontend/js/features/preview/components/preview-recompile-button.js
@@ -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({
{t('run_syntax_check_now')}
+
+
+ {t('stop_compile')}
+
{t('recompile_from_scratch')}
@@ -150,7 +149,7 @@ function PreviewRecompileButton({
placement="bottom"
overlay={
- {isCompiling || isClearingCache ? t('compiling') : t('recompile')}
+ {isCompiling ? t('compiling') : t('recompile')}
}
>
@@ -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
}
diff --git a/services/web/frontend/js/features/preview/components/preview-toolbar.js b/services/web/frontend/js/features/preview/components/preview-toolbar.js
index 7d6a84be78..5a464e531f 100644
--- a/services/web/frontend/js/features/preview/components/preview-toolbar.js
+++ b/services/web/frontend/js/features/preview/components/preview-toolbar.js
@@ -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 = (
+ {pdfExpandLabel}
+ )
+
+ function handlePdfExpandBtnClick() {
+ if (splitLayout) {
+ onSetFullLayout()
+ } else {
+ onSetSplitLayout()
+ }
+ }
+
useResizeObserver(toolbarRef, logsState, checkCanShowText)
return (
@@ -195,11 +217,12 @@ function PreviewToolbar({
+
+
+
+
+
)
@@ -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
}
diff --git a/services/web/frontend/js/ide/pdf/controllers/PdfController.js b/services/web/frontend/js/ide/pdf/controllers/PdfController.js
index 6286fabe62..58a69f1d8e 100644
--- a/services/web/frontend/js/ide/pdf/controllers/PdfController.js
+++ b/services/web/frontend/js/ide/pdf/controllers/PdfController.js
@@ -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
diff --git a/services/web/frontend/js/shared/hooks/use-expand-collapse.js b/services/web/frontend/js/shared/hooks/use-expand-collapse.js
index 65035b9f86..809aff9e2e 100644
--- a/services/web/frontend/js/shared/hooks/use-expand-collapse.js
+++ b/services/web/frontend/js/shared/hooks/use-expand-collapse.js
@@ -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
},
diff --git a/services/web/frontend/js/shared/hooks/use-resize-observer.js b/services/web/frontend/js/shared/hooks/use-resize-observer.js
index c88f0a6554..52c6266837 100644
--- a/services/web/frontend/js/shared/hooks/use-resize-observer.js
+++ b/services/web/frontend/js/shared/hooks/use-resize-observer.js
@@ -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)
+ }
}
}
},
diff --git a/services/web/frontend/stylesheets/app/editor/logs.less b/services/web/frontend/stylesheets/app/editor/logs.less
index 0cab1dd114..092883fc1c 100644
--- a/services/web/frontend/stylesheets/app/editor/logs.less
+++ b/services/web/frontend/stylesheets/app/editor/logs.less
@@ -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;
+ }
+}
diff --git a/services/web/frontend/stylesheets/app/editor/pdf.less b/services/web/frontend/stylesheets/app/editor/pdf.less
index f627dd563d..ed66f20933 100644
--- a/services/web/frontend/stylesheets/app/editor/pdf.less
+++ b/services/web/frontend/stylesheets/app/editor/pdf.less
@@ -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;
+ }
+}
diff --git a/services/web/frontend/stylesheets/components/alerts.less b/services/web/frontend/stylesheets/components/alerts.less
index ca398e7503..ecfc95adad 100755
--- a/services/web/frontend/stylesheets/components/alerts.less
+++ b/services/web/frontend/stylesheets/components/alerts.less
@@ -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 {
diff --git a/services/web/frontend/stylesheets/core/mixins.less b/services/web/frontend/stylesheets/core/mixins.less
index 8610aa96d0..68d30e7d23 100755
--- a/services/web/frontend/stylesheets/core/mixins.less
+++ b/services/web/frontend/stylesheets/core/mixins.less
@@ -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) {
diff --git a/services/web/frontend/stylesheets/core/variables.less b/services/web/frontend/stylesheets/core/variables.less
index 1481a332e2..479fa65b89 100644
--- a/services/web/frontend/stylesheets/core/variables.less
+++ b/services/web/frontend/stylesheets/core/variables.less
@@ -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
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 1b7de02f8b..1ec47f405b 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -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",
diff --git a/services/web/test/frontend/features/preview/components/preview-download-button.test.js b/services/web/test/frontend/features/preview/components/preview-download-button.test.js
index 0f5a6619d3..abd24c78a1 100644
--- a/services/web/test/frontend/features/preview/components/preview-download-button.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-download-button.test.js
@@ -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('
', 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('
', 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('
', 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
- })
- })
})
diff --git a/services/web/test/frontend/features/preview/components/preview-download-file-list.test.js b/services/web/test/frontend/features/preview/components/preview-download-file-list.test.js
new file mode 100644
index 0000000000..3805228e51
--- /dev/null
+++ b/services/web/test/frontend/features/preview/components/preview-download-file-list.test.js
@@ -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('
', 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(
)
+
+ 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(
)
+
+ 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(
)
+ 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(
)
+
+ 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(
)
+
+ 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(
)
+
+ expect(screen.queryByText('Other output files')).to.not.exist
+ expect(screen.queryByRole('separator')).to.not.exist
+ })
+ })
+})
diff --git a/services/web/test/frontend/features/preview/components/preview-logs-pane-entry.test.js b/services/web/test/frontend/features/preview/components/preview-logs-pane-entry.test.js
index 5608779656..3e30d6431e 100644
--- a/services/web/test/frontend/features/preview/components/preview-logs-pane-entry.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-logs-pane-entry.test.js
@@ -76,6 +76,42 @@ describe('
', 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(
)
screen.getByText(rawContent)
diff --git a/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js b/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js
index 5a5af82166..f7c04e4910 100644
--- a/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js
@@ -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(
+
+ )
+ })
+ 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(
-
- )
- })
- 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
)
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
)
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
)
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
)
const errorEntries = screen.queryAllByLabelText(
- 'There was an error compiling your project'
+ 'There was an error compiling this project'
)
expect(errorEntries).to.have.lengthOf(0)
})
diff --git a/services/web/test/frontend/features/preview/components/preview-logs-toggle-button.test.js b/services/web/test/frontend/features/preview/components/preview-logs-toggle-button.test.js
index e020a44a61..aebfa4582f 100644
--- a/services/web/test/frontend/features/preview/components/preview-logs-toggle-button.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-logs-toggle-button.test.js
@@ -60,7 +60,7 @@ describe('
', 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('
', 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('
', 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 = {
diff --git a/services/web/test/frontend/features/preview/components/preview-pane.test.js b/services/web/test/frontend/features/preview/components/preview-pane.test.js
index d6ace9100f..ce490a75f4 100644
--- a/services/web/test/frontend/features/preview/components/preview-pane.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-pane.test.js
@@ -33,7 +33,7 @@ describe('
', function() {
})
render(
)
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('
', function() {
render(
)
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('
', function() {
render(
)
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('
', function() {
render(
)
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('
', function() {
rerender(
)
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('
', function() {
)
rerender(
)
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('
', 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('
', function() {
rerender(
)
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('
', function() {
)
render(
)
- 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('
', function() {
render(
)
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('
', 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('
', function() {
isDraftModeOn: false,
isSyntaxCheckOn: false,
lastCompileTimestamp,
- logEntries,
+ logEntries: logEntriesWithDefaults,
compileFailed,
validationIssues,
errors
@@ -278,12 +289,17 @@ describe('
', function() {
onClearCache: () => {},
onLogEntryLocationClick: () => {},
onRecompile: () => {},
+ onRecompileFromScratch: () => {},
onRunSyntaxCheckNow: () => {},
onSetAutoCompile: () => {},
onSetDraftMode: () => {},
onSetSyntaxCheck: () => {},
onToggleLogs: () => {},
- showLogs: isShowingLogs
+ onSetSplitLayout: () => {},
+ onSetFullLayout: () => {},
+ onStopCompilation: () => {},
+ showLogs: isShowingLogs,
+ splitLayout: true
}
}
})
diff --git a/services/web/test/frontend/features/preview/components/preview-recompile-button.test.js b/services/web/test/frontend/features/preview/components/preview-recompile-button.test.js
index e31e810c15..59f641a355 100644
--- a/services/web/test/frontend/features/preview/components/preview-recompile-button.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-recompile-button.test.js
@@ -5,18 +5,19 @@ import PreviewRecompileButton from '../../../../../frontend/js/features/preview/
const { expect } = require('chai')
describe('
', 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('
', function() {
'Check syntax before compile',
"Don't check syntax",
'Run syntax check now',
+ 'Stop compilation',
'Recompile from scratch'
])
@@ -39,39 +41,17 @@ describe('
', 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('
', function() {
onSetAutoCompile={() => {}}
onSetDraftMode={() => {}}
onSetSyntaxCheck={() => {}}
- onClearCache={onClearCache}
+ onRecompileFromScratch={onRecompileFromScratch}
+ onStopCompilation={onStopCompilation}
showText={showText}
/>
)
diff --git a/services/web/test/frontend/features/preview/components/preview-toolbar.test.js b/services/web/test/frontend/features/preview/components/preview-toolbar.test.js
index 46fa9fbdf2..a75afc53d7 100644
--- a/services/web/test/frontend/features/preview/components/preview-toolbar.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-toolbar.test.js
@@ -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('
', 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(
', 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('
', 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('
', 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' })
+ })
})
diff --git a/services/web/test/frontend/shared/hooks/use-expand-collapse.test.js b/services/web/test/frontend/shared/hooks/use-expand-collapse.test.js
index 55df269ad4..e15d9c3b2d 100644
--- a/services/web/test/frontend/shared/hooks/use-expand-collapse.test.js
+++ b/services/web/test/frontend/shared/hooks/use-expand-collapse.test.js
@@ -14,6 +14,15 @@ const sampleContent = (
)
+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 = {