mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-13 13:43:41 +01:00
Merge pull request #3332 from overleaf/pr-new-log-entries-ui
New log entries ui GitOrigin-RevId: a13ccac04197e7c8f3777750b956d550c95c8f0d
This commit is contained in:
@@ -19,6 +19,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||
output-files="pdf.outputFiles"
|
||||
pdf-download-url="pdf.downloadUrl"
|
||||
show-logs="shouldShowLogs"
|
||||
on-log-entry-link-click="openInEditor"
|
||||
)
|
||||
else
|
||||
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")
|
||||
@@ -172,7 +173,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||
) #{translate("failed_compile_check_ignore")}
|
||||
| .
|
||||
|
||||
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
|
||||
div(ng-repeat="entry in pdf.logEntries.all")
|
||||
.alert(
|
||||
ng-class="{\
|
||||
'alert-danger': entry.level == 'error',\
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
[
|
||||
"download_pdf",
|
||||
"download_file",
|
||||
"n_warnings",
|
||||
"n_warnings_plural",
|
||||
"n_errors",
|
||||
"n_errors_plural",
|
||||
"file_outline",
|
||||
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
|
||||
"we_cant_find_any_sections_or_subsections_in_this_file",
|
||||
"find_out_more_about_the_file_outline",
|
||||
"expand",
|
||||
"collapse",
|
||||
"show_outline",
|
||||
"hide_outline",
|
||||
"compiling",
|
||||
"recompile",
|
||||
"auto_compile",
|
||||
"on",
|
||||
"off",
|
||||
"other_output_files",
|
||||
"collapse",
|
||||
"compile_mode",
|
||||
"normal",
|
||||
"compiling",
|
||||
"download_file",
|
||||
"download_pdf",
|
||||
"expand",
|
||||
"fast",
|
||||
"stop_on_validation_error",
|
||||
"file_outline",
|
||||
"find_out_more_about_the_file_outline",
|
||||
"hide_outline",
|
||||
"ignore_validation_errors",
|
||||
"run_syntax_check_now",
|
||||
"loading",
|
||||
"log_entry_description",
|
||||
"log_hint_extra_info",
|
||||
"n_errors_plural",
|
||||
"n_errors",
|
||||
"n_warnings_plural",
|
||||
"n_warnings",
|
||||
"navigate_log_source",
|
||||
"no_messages",
|
||||
"normal",
|
||||
"off",
|
||||
"on",
|
||||
"other_output_files",
|
||||
"recompile_from_scratch",
|
||||
"recompile",
|
||||
"run_syntax_check_now",
|
||||
"run_syntax_check_now",
|
||||
"send_first_message",
|
||||
"show_outline",
|
||||
"stop_on_validation_error",
|
||||
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
|
||||
"toggle_compile_options_menu",
|
||||
"toggle_output_files_list",
|
||||
"your_message",
|
||||
"your_project_has_errors",
|
||||
"view_warnings",
|
||||
"view_logs",
|
||||
"view_pdf",
|
||||
"recompile_from_scratch",
|
||||
"run_syntax_check_now",
|
||||
"toggle_compile_options_menu"
|
||||
"view_warnings",
|
||||
"we_cant_find_any_sections_or_subsections_in_this_file",
|
||||
"your_message",
|
||||
"your_project_has_errors"
|
||||
]
|
||||
|
||||
@@ -1,41 +1,178 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useExpandCollapse from '../../../shared/hooks/use-expand-collapse'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
function PreviewLogEntry({ file, line, message, content, raw, level }) {
|
||||
const logEntryClasses = classNames('alert', {
|
||||
'alert-danger': level === 'error',
|
||||
'alert-warning': level === 'warning',
|
||||
'alert-info': level === 'typesetting'
|
||||
function PreviewLogEntry({
|
||||
file,
|
||||
line,
|
||||
message,
|
||||
content,
|
||||
column,
|
||||
humanReadableHintComponent,
|
||||
extraInfoURL,
|
||||
level,
|
||||
onLogEntryLinkClick
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
function handleLogEntryLinkClick() {
|
||||
onLogEntryLinkClick({ file, line, column })
|
||||
}
|
||||
const logEntryDescription = t('log_entry_description', {
|
||||
level: level
|
||||
})
|
||||
return (
|
||||
<div className={logEntryClasses}>
|
||||
<span className="line-no">
|
||||
<Icon type="link" />
|
||||
{file ? <span>{file}</span> : null}
|
||||
{line ? <span>, {line}</span> : null}
|
||||
</span>
|
||||
{message ? (
|
||||
<p className="entry-message">
|
||||
{level} {message}
|
||||
</p>
|
||||
<div className="log-entry" aria-label={logEntryDescription}>
|
||||
<PreviewLogEntryHeader
|
||||
level={level}
|
||||
file={file}
|
||||
line={line}
|
||||
message={message}
|
||||
onLogEntryLinkClick={handleLogEntryLinkClick}
|
||||
/>
|
||||
{content ? (
|
||||
<PreviewLogEntryContent
|
||||
content={content}
|
||||
humanReadableHintComponent={humanReadableHintComponent}
|
||||
extraInfoURL={extraInfoURL}
|
||||
/>
|
||||
) : null}
|
||||
{content ? <p className="entry-content">{content.trim()}</p> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewLogEntryHeader({
|
||||
level,
|
||||
file,
|
||||
line,
|
||||
message,
|
||||
onLogEntryLinkClick
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const logEntryHeaderClasses = classNames('log-entry-header', {
|
||||
'log-entry-header-error': level === 'error',
|
||||
'log-entry-header-warning': level === 'warning',
|
||||
'log-entry-header-typesetting': level === 'typesetting'
|
||||
})
|
||||
const headerLinkBtnTitle = t('navigate_log_source', {
|
||||
location: file + (line ? `, ${line}` : '')
|
||||
})
|
||||
return (
|
||||
<header className={logEntryHeaderClasses}>
|
||||
<h3 className="log-entry-header-title">{message}</h3>
|
||||
{file ? (
|
||||
<button
|
||||
className="btn-inline-link log-entry-header-link"
|
||||
type="button"
|
||||
title={headerLinkBtnTitle}
|
||||
onClick={onLogEntryLinkClick}
|
||||
>
|
||||
<Icon type="chain" />
|
||||
|
||||
<span>{file}</span>
|
||||
{line ? <span>, {line}</span> : null}
|
||||
</button>
|
||||
) : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewLogEntryContent({
|
||||
content,
|
||||
humanReadableHintComponent,
|
||||
extraInfoURL
|
||||
}) {
|
||||
const { isExpanded, expandableProps, toggleProps } = useExpandCollapse({
|
||||
collapsedSize: 150,
|
||||
classes: {
|
||||
container: 'log-entry-content-raw-expandable-container'
|
||||
}
|
||||
})
|
||||
const logContentClasses = classNames('log-entry-content-raw', {
|
||||
'log-entry-content-raw-collapsed': !isExpanded
|
||||
})
|
||||
const buttonContainerClasses = classNames(
|
||||
'log-entry-content-button-container',
|
||||
{
|
||||
'log-entry-content-button-container-collapsed': !isExpanded
|
||||
}
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="log-entry-content">
|
||||
<div {...expandableProps}>
|
||||
<pre className={logContentClasses}>{content.trim()}</pre>
|
||||
<div className={buttonContainerClasses}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-default log-entry-btn-expand-collapse"
|
||||
{...toggleProps}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<Icon type="angle-up" /> {t('collapse')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon type="angle-down" /> {t('expand')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{humanReadableHintComponent ? (
|
||||
<div className="log-entry-human-readable-hint">
|
||||
{humanReadableHintComponent}
|
||||
</div>
|
||||
) : null}
|
||||
{extraInfoURL ? (
|
||||
<div className="log-entry-human-readable-hint-link">
|
||||
<a
|
||||
href={extraInfoURL}
|
||||
target="_blank"
|
||||
className="log-entry-human-readable-hint-link"
|
||||
>
|
||||
{t('log_hint_extra_info')}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
PreviewLogEntryHeader.propTypes = {
|
||||
level: PropTypes.string.isRequired,
|
||||
file: PropTypes.string,
|
||||
line: PropTypes.any,
|
||||
message: PropTypes.string,
|
||||
onLogEntryLinkClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
PreviewLogEntryContent.propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
humanReadableHintComponent: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.element
|
||||
]),
|
||||
extraInfoURL: PropTypes.string
|
||||
}
|
||||
|
||||
PreviewLogEntry.propTypes = {
|
||||
file: PropTypes.string,
|
||||
// `line should be either a number or null (i.e. not required), but currently sometimes we get
|
||||
// an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
|
||||
// to PropTypes.number (not required) once we fix that.
|
||||
line: PropTypes.any,
|
||||
column: PropTypes.any,
|
||||
message: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
raw: PropTypes.string,
|
||||
level: PropTypes.string.isRequired
|
||||
humanReadableHintComponent: PropTypes.node,
|
||||
extraInfoURL: PropTypes.string,
|
||||
level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
|
||||
onLogEntryLinkClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default PreviewLogEntry
|
||||
|
||||
@@ -2,12 +2,16 @@ import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import PreviewLogEntry from './preview-log-entry'
|
||||
|
||||
function PreviewLogsPane({ logEntries }) {
|
||||
function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
|
||||
return (
|
||||
<div className="pdf-logs">
|
||||
<div className="logs-pane">
|
||||
{logEntries && logEntries.length > 0 ? (
|
||||
logEntries.map((logEntry, idx) => (
|
||||
<PreviewLogEntry key={idx} {...logEntry} />
|
||||
<PreviewLogEntry
|
||||
key={idx}
|
||||
{...logEntry}
|
||||
onLogEntryLinkClick={onLogEntryLinkClick}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div>No logs</div>
|
||||
@@ -17,7 +21,8 @@ function PreviewLogsPane({ logEntries }) {
|
||||
}
|
||||
|
||||
PreviewLogsPane.propTypes = {
|
||||
logEntries: PropTypes.array
|
||||
logEntries: PropTypes.array,
|
||||
onLogEntryLinkClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default PreviewLogsPane
|
||||
|
||||
@@ -15,10 +15,10 @@ function PreviewPane({
|
||||
onToggleLogs,
|
||||
outputFiles,
|
||||
pdfDownloadUrl,
|
||||
onLogEntryLinkClick,
|
||||
showLogs
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nErrors =
|
||||
compilerState.logEntries && compilerState.logEntries.errors
|
||||
? compilerState.logEntries.errors.length
|
||||
@@ -59,7 +59,10 @@ function PreviewPane({
|
||||
: ''}
|
||||
</span>
|
||||
{showLogs ? (
|
||||
<PreviewLogsPane logEntries={compilerState.logEntries.all} />
|
||||
<PreviewLogsPane
|
||||
logEntries={compilerState.logEntries.all}
|
||||
onLogEntryLinkClick={onLogEntryLinkClick}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
@@ -74,6 +77,7 @@ PreviewPane.propTypes = {
|
||||
logEntries: PropTypes.object.isRequired
|
||||
}),
|
||||
onClearCache: PropTypes.func.isRequired,
|
||||
onLogEntryLinkClick: PropTypes.func.isRequired,
|
||||
onRecompile: PropTypes.func.isRequired,
|
||||
onRunSyntaxCheckNow: PropTypes.func.isRequired,
|
||||
onSetAutoCompile: PropTypes.func.isRequired,
|
||||
|
||||
@@ -878,6 +878,25 @@ App.controller('PdfController', function(
|
||||
$scope.recompile({ check: true })
|
||||
})
|
||||
}
|
||||
|
||||
$scope.openInEditor = function(entry) {
|
||||
let column, line
|
||||
eventTracking.sendMBOnce('logs-jump-to-location-once')
|
||||
const entity = ide.fileTreeManager.findEntityByPath(entry.file)
|
||||
if (entity == null || entity.type !== 'doc') {
|
||||
return
|
||||
}
|
||||
if (entry.line != null) {
|
||||
line = entry.line
|
||||
}
|
||||
if (entry.column != null) {
|
||||
column = entry.column
|
||||
}
|
||||
ide.editorManager.openDoc(entity, {
|
||||
gotoLine: line,
|
||||
gotoColumn: column
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
App.factory('synctex', function(ide, $http, $q) {
|
||||
@@ -1048,27 +1067,6 @@ App.controller('PdfSynctexController', function($scope, synctex, ide) {
|
||||
}
|
||||
})
|
||||
|
||||
App.controller('PdfLogEntryController', function($scope, ide, eventTracking) {
|
||||
$scope.openInEditor = function(entry) {
|
||||
let column, line
|
||||
eventTracking.sendMBOnce('logs-jump-to-location-once')
|
||||
const entity = ide.fileTreeManager.findEntityByPath(entry.file)
|
||||
if (entity == null || entity.type !== 'doc') {
|
||||
return
|
||||
}
|
||||
if (entry.line != null) {
|
||||
line = entry.line
|
||||
}
|
||||
if (entry.column != null) {
|
||||
column = entry.column
|
||||
}
|
||||
ide.editorManager.openDoc(entity, {
|
||||
gotoLine: line,
|
||||
gotoColumn: column
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
App.controller('ClearCacheModalController', function($scope, $modalInstance) {
|
||||
$scope.state = { error: false, inflight: false }
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useRef, useState, useLayoutEffect } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
function useExpandCollapse({
|
||||
initiallyExpanded = false,
|
||||
collapsedSize = '0',
|
||||
dimension = 'height',
|
||||
classes = {}
|
||||
} = {}) {
|
||||
const ref = useRef()
|
||||
const [isExpanded, setIsExpanded] = useState(initiallyExpanded)
|
||||
const [size, setSize] = useState()
|
||||
|
||||
useLayoutEffect(
|
||||
() => {
|
||||
const expandCollapseEl = ref.current
|
||||
if (isExpanded) {
|
||||
const expandedSize =
|
||||
dimension === 'height'
|
||||
? expandCollapseEl.scrollHeight
|
||||
: expandCollapseEl.scrollWidth
|
||||
setSize(expandedSize)
|
||||
} else {
|
||||
setSize(collapsedSize)
|
||||
}
|
||||
},
|
||||
[isExpanded]
|
||||
)
|
||||
|
||||
const expandableClasses = classNames(
|
||||
'expand-collapse-container',
|
||||
classes.container,
|
||||
!isExpanded ? classes.containerCollapsed : null
|
||||
)
|
||||
|
||||
function handleToggle() {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
|
||||
return {
|
||||
isExpanded,
|
||||
expandableProps: {
|
||||
ref,
|
||||
style: {
|
||||
[dimension === 'height' ? 'height' : 'width']: `${size}px`
|
||||
},
|
||||
className: expandableClasses
|
||||
},
|
||||
toggleProps: {
|
||||
onClick: handleToggle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default useExpandCollapse
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from 'react'
|
||||
import PreviewLogEntry from '../js/features/preview/components/preview-log-entry.js'
|
||||
|
||||
export const ErrorWithCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
ErrorWithCompilerOutput.args = {
|
||||
level: 'error'
|
||||
}
|
||||
|
||||
export const ErrorWithCompilerOutputAndHumanReadableHint = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
)
|
||||
ErrorWithCompilerOutputAndHumanReadableHint.args = {
|
||||
level: 'error',
|
||||
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
|
||||
}
|
||||
|
||||
export const ErrorWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
ErrorWithoutCompilerOutput.args = {
|
||||
level: 'error',
|
||||
content: null
|
||||
}
|
||||
|
||||
export const WarningWithCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
WarningWithCompilerOutput.args = {
|
||||
level: 'warning'
|
||||
}
|
||||
|
||||
export const WarningWithCompilerOutputAndHumanReadableHint = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
)
|
||||
WarningWithCompilerOutputAndHumanReadableHint.args = {
|
||||
level: 'warning',
|
||||
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
|
||||
}
|
||||
|
||||
export const WarningWithoutCompilerOutput = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
)
|
||||
WarningWithoutCompilerOutput.args = {
|
||||
level: 'warning',
|
||||
content: null
|
||||
}
|
||||
|
||||
export const InfoWithCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
InfoWithCompilerOutput.args = {
|
||||
level: 'typesetting'
|
||||
}
|
||||
|
||||
export const InfoWithCompilerOutputAndHumanReadableHint = args => (
|
||||
<PreviewLogEntry {...args} />
|
||||
)
|
||||
InfoWithCompilerOutputAndHumanReadableHint.args = {
|
||||
level: 'typesetting',
|
||||
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
|
||||
}
|
||||
|
||||
export const InfoWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
|
||||
InfoWithoutCompilerOutput.args = {
|
||||
level: 'typesetting',
|
||||
content: null
|
||||
}
|
||||
|
||||
function SampleHumanReadableHintComponent() {
|
||||
return (
|
||||
<>
|
||||
Human-readable hint to help LaTeX newbies. Supports <b>mark-up</b> for
|
||||
e.g.{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr"
|
||||
>
|
||||
inline links
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'PreviewLogEntry',
|
||||
component: PreviewLogEntry,
|
||||
args: {
|
||||
file: 'foo/bar.tex',
|
||||
line: 10,
|
||||
column: 20,
|
||||
message: 'Lorem ipsum',
|
||||
content: `
|
||||
The LaTeX compiler output
|
||||
* With a lot of details
|
||||
|
||||
Wrapped in an HTML <pre> element with
|
||||
preformatted text which is to be presented exactly
|
||||
as written in the HTML file
|
||||
|
||||
(whitespace included™)
|
||||
|
||||
The text is typically rendered using a non-proportional ("monospace") font.
|
||||
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <7> on input line 18.
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <5> on input line 18.
|
||||
! Undefined control sequence.
|
||||
<recently read> \\Zlpha
|
||||
|
||||
main.tex, line 23
|
||||
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useExpandCollapse from '../js/shared/hooks/use-expand-collapse'
|
||||
|
||||
function TestUI({ children, expandableProps, toggleProps }) {
|
||||
return (
|
||||
<>
|
||||
<div {...expandableProps}>{children}</div>
|
||||
<button {...toggleProps}>Expand/collapse</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function VerticalTestUI(props) {
|
||||
return <TestUI {...props}>{verticalContents}</TestUI>
|
||||
}
|
||||
|
||||
function HorizontalTestUI(props) {
|
||||
return <TestUI {...props}>{horizontalContents}</TestUI>
|
||||
}
|
||||
|
||||
VerticalTestUI.propTypes = {
|
||||
expandableProps: PropTypes.object,
|
||||
toggleProps: PropTypes.object
|
||||
}
|
||||
|
||||
HorizontalTestUI.propTypes = VerticalTestUI.propTypes
|
||||
|
||||
TestUI.propTypes = {
|
||||
...VerticalTestUI.propTypes,
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
export const Vertical = args => {
|
||||
const { expandableProps, toggleProps } = useExpandCollapse(args)
|
||||
return (
|
||||
<VerticalTestUI
|
||||
expandableProps={expandableProps}
|
||||
toggleProps={toggleProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const VerticalInitiallyExpanded = args => {
|
||||
const { expandableProps, toggleProps } = useExpandCollapse(args)
|
||||
return (
|
||||
<VerticalTestUI
|
||||
expandableProps={expandableProps}
|
||||
toggleProps={toggleProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
VerticalInitiallyExpanded.args = {
|
||||
initiallyExpanded: true
|
||||
}
|
||||
|
||||
export const VerticalWithMinSize = args => {
|
||||
const { expandableProps, toggleProps } = useExpandCollapse(args)
|
||||
return (
|
||||
<VerticalTestUI
|
||||
expandableProps={expandableProps}
|
||||
toggleProps={toggleProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
VerticalWithMinSize.args = {
|
||||
collapsedSize: 200
|
||||
}
|
||||
|
||||
export const Horizontal = args => {
|
||||
const { expandableProps, toggleProps } = useExpandCollapse(args)
|
||||
return (
|
||||
<HorizontalTestUI
|
||||
expandableProps={expandableProps}
|
||||
toggleProps={toggleProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Horizontal.args = {
|
||||
dimension: 'width'
|
||||
}
|
||||
|
||||
export const HorizontalInitiallyExpanded = args => {
|
||||
const { expandableProps, toggleProps } = useExpandCollapse(args)
|
||||
return (
|
||||
<HorizontalTestUI
|
||||
expandableProps={expandableProps}
|
||||
toggleProps={toggleProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
HorizontalInitiallyExpanded.args = {
|
||||
initiallyExpanded: true
|
||||
}
|
||||
|
||||
export const HorizontalWithMinSize = args => {
|
||||
const { expandableProps, toggleProps } = useExpandCollapse(args)
|
||||
return (
|
||||
<HorizontalTestUI
|
||||
expandableProps={expandableProps}
|
||||
toggleProps={toggleProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
HorizontalWithMinSize.args = {
|
||||
dimension: 'width',
|
||||
collapsedSize: 200
|
||||
}
|
||||
|
||||
const defaultContentStyles = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '2em',
|
||||
backgroundImage: 'linear-gradient(to bottom, red, blue)',
|
||||
width: '500px',
|
||||
height: '500px',
|
||||
color: '#FFF'
|
||||
}
|
||||
|
||||
const verticalContents = <div style={defaultContentStyles}>Vertical</div>
|
||||
|
||||
const horizontalContents = (
|
||||
<div
|
||||
style={{
|
||||
...defaultContentStyles,
|
||||
backgroundImage: 'linear-gradient(to right, red, blue)'
|
||||
}}
|
||||
>
|
||||
Horizontal
|
||||
</div>
|
||||
)
|
||||
|
||||
export default {
|
||||
title: 'useExpandCollapse'
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
@import 'components/nvd3.less';
|
||||
@import 'components/nvd3_override.less';
|
||||
@import 'components/infinite-scroll.less';
|
||||
@import 'components/expand-collapse.less';
|
||||
|
||||
// Components w/ JavaScript
|
||||
@import 'components/modals.less';
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
@import './editor/rich-text.less';
|
||||
@import './editor/publish-modal.less';
|
||||
@import './editor/outline.less';
|
||||
@import './editor/logs.less';
|
||||
|
||||
@ui-layout-toggler-def-height: 50px;
|
||||
@ui-resizer-size: 7px;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
.logs-pane {
|
||||
.full-size;
|
||||
top: @pdf-top-offset;
|
||||
padding: @padding-sm;
|
||||
overflow-y: auto;
|
||||
background-color: @logs-pane-bg;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: @margin-sm;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-header-error {
|
||||
background-color: @ol-red;
|
||||
}
|
||||
|
||||
.log-entry-header-warning {
|
||||
background-color: @orange;
|
||||
}
|
||||
|
||||
.log-entry-header-typesetting {
|
||||
background-color: @ol-blue;
|
||||
}
|
||||
|
||||
.log-entry-header-title,
|
||||
.log-entry-header-link {
|
||||
font-family: @font-family-sans-serif;
|
||||
font-size: @font-size-base;
|
||||
line-height: @line-height-base;
|
||||
font-weight: 700;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.log-entry-header-link {
|
||||
border-width: 0;
|
||||
flex-grow: 0;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
padding-left: @padding-sm;
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
color: #fff;
|
||||
}
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: @ol-blue-gray-1;
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
|
||||
.log-entry-content-raw-button-container-collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.log-entry-content-raw {
|
||||
font-size: @font-size-small;
|
||||
color: @ol-blue-gray-4;
|
||||
padding: @padding-sm @padding-sm @padding-xl @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;
|
||||
height: 40px;
|
||||
padding-bottom: @padding-sm;
|
||||
text-align: center;
|
||||
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-btn-expand-collapse {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
&:focus,
|
||||
&:focus:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-human-readable-hint,
|
||||
.log-entry-human-readable-hint-link {
|
||||
font-size: @font-size-small;
|
||||
margin-top: @margin-sm;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.expand-collapse-container {
|
||||
overflow: hidden;
|
||||
transition: height 0.15s ease-in-out, width 0.15s ease-in-out;
|
||||
}
|
||||
@@ -89,6 +89,7 @@
|
||||
@editor-header-logo-background : url(/img/ol-brand/overleaf-o.svg) center / contain no-repeat;
|
||||
@project-name-color : @ol-blue-gray-3;
|
||||
@pdf-bg : @ol-blue-gray-0;
|
||||
@logs-pane-bg: @ol-blue-gray-1;
|
||||
|
||||
// Navbar
|
||||
@navbar-default-bg : #FFF;
|
||||
|
||||
@@ -1060,11 +1060,12 @@
|
||||
@chat-new-message-textarea-color: @ol-blue-gray-6;
|
||||
@chat-new-message-border-color: @editor-border-color;
|
||||
|
||||
// PDF
|
||||
// PDF and logs
|
||||
@pdf-top-offset: @toolbar-small-height;
|
||||
@pdf-bg: @ol-blue-gray-1;
|
||||
@pdfjs-bg: transparent;
|
||||
@pdf-page-shadow-color: rgba(0, 0, 0, 0.5);
|
||||
@logs-pane-bg: @ol-blue-gray-5;
|
||||
@log-line-no-color: #fff;
|
||||
@log-hints-color: @ol-blue-gray-4;
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"log_entry_description": "Log entry with level: \"__level__\"",
|
||||
"navigate_log_source": "Navigate to log position in source code: __location__",
|
||||
"other_output_files": "Other output files",
|
||||
"toggle_output_files_list": "Toggle output files list",
|
||||
"n_warnings": "__count__ warning",
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import React from 'react'
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { screen, render, fireEvent } from '@testing-library/react'
|
||||
|
||||
import PreviewLogEntry from '../../../../../frontend/js/features/preview/components/preview-log-entry.js'
|
||||
|
||||
describe('<PreviewLogEntry />', function() {
|
||||
const level = 'error'
|
||||
const noOp = () => {}
|
||||
|
||||
describe('log entry description', function() {
|
||||
for (const level of ['error', 'warning', 'typesetting']) {
|
||||
it(`describes the log entry with ${level} information`, function() {
|
||||
render(<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />)
|
||||
screen.getByLabelText(`Log entry with level: "${level}"`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('log location link', function() {
|
||||
const file = 'foo.tex'
|
||||
const line = 42
|
||||
const column = 21
|
||||
const onLogEntryLinkClick = sinon.stub()
|
||||
|
||||
afterEach(function() {
|
||||
onLogEntryLinkClick.reset()
|
||||
})
|
||||
|
||||
it('renders both file and line', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
file={file}
|
||||
line={line}
|
||||
level={level}
|
||||
onLogEntryLinkClick={noOp}
|
||||
/>
|
||||
)
|
||||
screen.getByRole('button', {
|
||||
name: `Navigate to log position in source code: ${file}, ${line}`
|
||||
})
|
||||
})
|
||||
|
||||
it('renders only file when line information is not available', function() {
|
||||
render(
|
||||
<PreviewLogEntry file={file} level={level} onLogEntryLinkClick={noOp} />
|
||||
)
|
||||
screen.getByRole('button', {
|
||||
name: `Navigate to log position in source code: ${file}`
|
||||
})
|
||||
})
|
||||
|
||||
it('does not render when file information is not available', function() {
|
||||
render(<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />)
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: `Navigate to log position in source code: `
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
||||
it('calls the callback with file, line and column on click', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
file={file}
|
||||
line={line}
|
||||
column={column}
|
||||
level={level}
|
||||
onLogEntryLinkClick={onLogEntryLinkClick}
|
||||
/>
|
||||
)
|
||||
const linkToSourceButton = screen.getByRole('button', {
|
||||
name: `Navigate to log position in source code: ${file}, ${line}`
|
||||
})
|
||||
|
||||
fireEvent.click(linkToSourceButton)
|
||||
expect(onLogEntryLinkClick).to.be.calledOnce
|
||||
expect(onLogEntryLinkClick).to.be.calledWith({
|
||||
file,
|
||||
line: line,
|
||||
column: column
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('log entry contents', function() {
|
||||
const logContent = 'foo bar latex error stuff baz'
|
||||
|
||||
it('renders collapsed contents by default', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
level={level}
|
||||
onLogEntryLinkClick={noOp}
|
||||
/>
|
||||
)
|
||||
screen.getByText(logContent)
|
||||
screen.getByRole('button', {
|
||||
name: 'Expand'
|
||||
})
|
||||
})
|
||||
|
||||
it('supports expanding contents', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
level={level}
|
||||
onLogEntryLinkClick={noOp}
|
||||
/>
|
||||
)
|
||||
screen.getByText(logContent)
|
||||
const expandCollapseBtn = screen.getByRole('button', {
|
||||
name: 'Expand'
|
||||
})
|
||||
fireEvent.click(expandCollapseBtn)
|
||||
screen.getByRole('button', {
|
||||
name: 'Collapse'
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render at all when there are no log contents', function() {
|
||||
const { container } = render(
|
||||
<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />
|
||||
)
|
||||
expect(container.querySelector('.log-entry-content')).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('human-readable hints', function() {
|
||||
const logContent = 'foo bar latex error stuff baz'
|
||||
const logHintText = 'foo bar baz'
|
||||
const logHint = <>{logHintText}</>
|
||||
const infoURL = 'www.overleaf.com/learn/latex'
|
||||
|
||||
it('renders the hint', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
humanReadableHintComponent={logHint}
|
||||
extraInfoURL={infoURL}
|
||||
level={level}
|
||||
onLogEntryLinkClick={noOp}
|
||||
/>
|
||||
)
|
||||
screen.getByText(logHintText)
|
||||
})
|
||||
|
||||
it('renders the link to learn more', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
humanReadableHintComponent={logHint}
|
||||
extraInfoURL={infoURL}
|
||||
level={level}
|
||||
onLogEntryLinkClick={noOp}
|
||||
/>
|
||||
)
|
||||
screen.getByRole('link', { name: 'Learn more' })
|
||||
})
|
||||
|
||||
it('does not render the link when it is not available', function() {
|
||||
render(
|
||||
<PreviewLogEntry
|
||||
content={logContent}
|
||||
humanReadableHintComponent={logHint}
|
||||
level={level}
|
||||
onLogEntryLinkClick={noOp}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { expect } from 'chai'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import useExpandCollapse from '../../../../frontend/js/shared/hooks/use-expand-collapse'
|
||||
|
||||
const sampleContent = (
|
||||
<div>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
||||
<p>Maecenas maximus ultrices sollicitudin.</p>
|
||||
<p>Praesent mollis arcu eget molestie viverra.</p>
|
||||
<p>Pellentesque eget molestie nisl, non hendrerit lectus.</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
function ExpandCollapseTestUI({ expandCollapseArgs }) {
|
||||
const { expandableProps } = useExpandCollapse(expandCollapseArgs)
|
||||
return (
|
||||
<>
|
||||
<div {...expandableProps}>{sampleContent}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ExpandCollapseTestUI.propTypes = {
|
||||
expandCollapseArgs: PropTypes.object
|
||||
}
|
||||
|
||||
describe('useExpandCollapse', function() {
|
||||
describe('custom CSS classes', function() {
|
||||
it('supports a custom CSS class', function() {
|
||||
const testArgs = {
|
||||
classes: {
|
||||
container: 'my-custom-class'
|
||||
}
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const elWithCustomCSSClass = container.querySelector('div')
|
||||
expect(elWithCustomCSSClass).to.exist
|
||||
})
|
||||
it('supports an extra custom CSS class for the collapsed state', function() {
|
||||
const testArgs = {
|
||||
classes: {
|
||||
containerCollapsed: 'my-custom-collapsed-class'
|
||||
}
|
||||
}
|
||||
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const elWithCustomCollapsedCSSClass = container.querySelector(
|
||||
'.my-custom-collapsed-class'
|
||||
)
|
||||
expect(elWithCustomCollapsedCSSClass).to.exist
|
||||
})
|
||||
it('ignores the collapsed CSS class when expanded', function() {
|
||||
const testArgs = {
|
||||
initiallyExpanded: true,
|
||||
classes: {
|
||||
containerCollapsed: 'my-custom-collapsed-class'
|
||||
}
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const elWithCustomCollapsedCSSClass = container.querySelector(
|
||||
'.my-custom-collapsed-class'
|
||||
)
|
||||
expect(elWithCustomCollapsedCSSClass).to.not.exist
|
||||
})
|
||||
})
|
||||
describe('height and width support via dimension argument', function() {
|
||||
it('defaults to height', function() {
|
||||
const { container } = render(<ExpandCollapseTestUI />)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.not.be.empty
|
||||
expect(expandCollapseEl.style.width).to.be.empty
|
||||
})
|
||||
it('supports width', function() {
|
||||
const testArgs = {
|
||||
dimension: 'width'
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.be.empty
|
||||
expect(expandCollapseEl.style.width).to.not.be.empty
|
||||
})
|
||||
})
|
||||
describe('collapsed size support via collapsedSize argument', function() {
|
||||
it('defaults to 0px', function() {
|
||||
const { container } = render(<ExpandCollapseTestUI />)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.equal('0px')
|
||||
})
|
||||
it('supports a custom collapsed size', function() {
|
||||
const testArgs = {
|
||||
collapsedSize: 200
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.equal('200px')
|
||||
})
|
||||
it('supports a custom collapsed size for width', function() {
|
||||
const testArgs = {
|
||||
collapsedSize: 200,
|
||||
dimension: 'width'
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.be.empty
|
||||
expect(expandCollapseEl.style.width).to.equal('200px')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user