Add hybrid toolbar to migrated PDF preview (#5414)

GitOrigin-RevId: 6266028091229c819aee3c8d4bd3bff2e2417125
This commit is contained in:
Alf Eaton 2021-10-12 09:47:46 +01:00 committed by Copybot
parent 7f7e5ed749
commit e26d47cb41
15 changed files with 364 additions and 43 deletions

View file

@ -55,8 +55,8 @@
"conflicting_paths_found": "", "conflicting_paths_found": "",
"connected_users": "", "connected_users": "",
"contact_message_label": "", "contact_message_label": "",
"continue_github_merge": "",
"contact_us": "", "contact_us": "",
"continue_github_merge": "",
"copy": "", "copy": "",
"copy_project": "", "copy_project": "",
"copying": "", "copying": "",
@ -179,9 +179,10 @@
"log_entry_maximum_entries_message": "", "log_entry_maximum_entries_message": "",
"log_entry_maximum_entries_title": "", "log_entry_maximum_entries_title": "",
"log_hint_extra_info": "", "log_hint_extra_info": "",
"log_viewer_error": "",
"logs_and_output_files": "",
"logs_pane_info_message": "", "logs_pane_info_message": "",
"logs_pane_info_message_popup": "", "logs_pane_info_message_popup": "",
"log_viewer_error": "",
"main_file_not_found": "", "main_file_not_found": "",
"make_private": "", "make_private": "",
"manage_files_from_your_dropbox_folder": "", "manage_files_from_your_dropbox_folder": "",
@ -257,7 +258,7 @@
"project_too_large": "", "project_too_large": "",
"project_too_large_please_reduce": "", "project_too_large_please_reduce": "",
"project_too_much_editable_text": "", "project_too_much_editable_text": "",
"project_url" : "", "project_url": "",
"public": "", "public": "",
"pull_github_changes_into_sharelatex": "", "pull_github_changes_into_sharelatex": "",
"push_sharelatex_changes_to_github": "", "push_sharelatex_changes_to_github": "",
@ -269,6 +270,7 @@
"recent_commits_in_github": "", "recent_commits_in_github": "",
"recompile": "", "recompile": "",
"recompile_from_scratch": "", "recompile_from_scratch": "",
"recompile_pdf": "",
"reconnect": "", "reconnect": "",
"reference_error_relink_hint": "", "reference_error_relink_hint": "",
"refresh": "", "refresh": "",

View file

@ -1,4 +1,10 @@
import { Button, Dropdown, MenuItem } from 'react-bootstrap' import {
Button,
Dropdown,
MenuItem,
OverlayTrigger,
Tooltip,
} from 'react-bootstrap'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import ControlledDropdown from '../../../shared/components/controlled-dropdown' import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -6,6 +12,8 @@ import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
import { memo } from 'react' import { memo } from 'react'
import classnames from 'classnames' import classnames from 'classnames'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
function PdfCompileButton() { function PdfCompileButton() {
const { const {
autoCompile, autoCompile,
@ -34,17 +42,28 @@ function PdfCompileButton() {
})} })}
id="pdf-recompile-dropdown" id="pdf-recompile-dropdown"
> >
<Button <OverlayTrigger
className="btn-recompile" placement="bottom"
bsStyle="success" delayShow={500}
onClick={compiling ? stopCompile : startCompile} overlay={
aria-label={compileButtonLabel} <Tooltip id="tooltip-logs-toggle" className="keyboard-tooltip">
{t('recompile_pdf')}{' '}
<span className="keyboard-shortcut">({modifierKey} + Enter)</span>
</Tooltip>
}
> >
<Icon type="refresh" spin={compiling} /> <Button
<span className="toolbar-text toolbar-hide-medium toolbar-hide-small"> className="btn-recompile"
{compileButtonLabel} bsStyle="success"
</span> onClick={compiling ? stopCompile : startCompile}
</Button> aria-label={compileButtonLabel}
>
<Icon type="refresh" spin={compiling} />
<span className="toolbar-hide-medium toolbar-hide-small btn-recompile-label">
{compileButtonLabel}
</span>
</Button>
</OverlayTrigger>
<Dropdown.Toggle <Dropdown.Toggle
aria-label={t('toggle_compile_options_menu')} aria-label={t('toggle_compile_options_menu')}

View file

@ -0,0 +1,43 @@
import { memo, useCallback } from 'react'
import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
import { sendMBOnce } from '../../../infrastructure/event-tracking'
import { Button } from 'react-bootstrap'
import Icon from '../../../shared/components/icon'
import { useTranslation } from 'react-i18next'
function PdfHybridCodeCheckButton() {
const { codeCheckFailed, error, setShowLogs } = usePdfPreviewContext()
const { t } = useTranslation()
const handleClick = useCallback(() => {
setShowLogs(value => {
if (!value) {
sendMBOnce('ide-open-logs-once')
}
return !value
})
}, [setShowLogs])
if (!codeCheckFailed) {
return null
}
return (
<Button
bsSize="xsmall"
bsStyle="danger"
disabled={Boolean(error)}
className="btn-toggle-logs toolbar-item"
onClick={handleClick}
>
<Icon type="exclamation-triangle" />
<span className="toolbar-text toolbar-hide-small">
{t('code_check_failed')}
</span>
</Button>
)
}
export default memo(PdfHybridCodeCheckButton)

View file

@ -0,0 +1,36 @@
import { useTranslation } from 'react-i18next'
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
import Icon from '../../../shared/components/icon'
import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
import { memo } from 'react'
function PdfHybridDownloadButton() {
const { pdfDownloadUrl } = usePdfPreviewContext()
const { t } = useTranslation()
return (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="tooltip-logs-toggle">
{pdfDownloadUrl
? t('download_pdf')
: t('please_compile_pdf_before_download')}
</Tooltip>
}
>
<Button
bsStyle="link"
disabled={!pdfDownloadUrl}
download
href={pdfDownloadUrl || '#'}
target="_blank"
>
<Icon type="download" modifier="fw" />
</Button>
</OverlayTrigger>
)
}
export default memo(PdfHybridDownloadButton)

View file

@ -0,0 +1,55 @@
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Label, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
import { sendMBOnce } from '../../../infrastructure/event-tracking'
import Icon from '../../../shared/components/icon'
function PdfHybridLogsButton() {
const { error, logEntries, setShowLogs, showLogs } = usePdfPreviewContext()
const { t } = useTranslation()
const handleClick = useCallback(() => {
setShowLogs(value => {
if (!value) {
sendMBOnce('ide-open-logs-once')
}
return !value
})
}, [setShowLogs])
const errorCount = Number(logEntries?.errors?.length)
const warningCount = Number(logEntries?.warnings?.length)
const totalCount = errorCount + warningCount
return (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="tooltip-logs-toggle">{t('logs_and_output_files')}</Tooltip>
}
>
<Button
bsStyle="link"
disabled={Boolean(error)}
active={showLogs}
className="toolbar-item log-btn"
onClick={handleClick}
style={{ position: 'relative' }}
aria-label={showLogs ? t('view_pdf') : t('view_logs')}
>
<Icon type="file-text-o" modifier="fw" />
{!showLogs && totalCount > 0 && (
<Label bsStyle={errorCount === 0 ? 'warning' : 'danger'}>
{totalCount}
</Label>
)}
</Button>
</OverlayTrigger>
)
}
export default memo(PdfHybridLogsButton)

View file

@ -1,4 +1,3 @@
import Icon from '../../../shared/components/icon'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry' import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry'
import { usePdfPreviewContext } from '../contexts/pdf-preview-context' import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
@ -11,7 +10,8 @@ import PdfDownloadFilesButton from './pdf-download-files-button'
import PdfLogsEntries from './pdf-logs-entries' import PdfLogsEntries from './pdf-logs-entries'
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '../../../infrastructure/error-boundary'
import ErrorBoundaryFallback from './error-boundary-fallback' import ErrorBoundaryFallback from './error-boundary-fallback'
import { LogsPaneInfoNotice } from '../../preview/components/preview-logs-pane' import PdfCodeCheckFailedNotice from '../../preview/components/pdf-code-check-failed-notice'
import PdfLogsPaneInfoNotice from '../../preview/components/pdf-logs-pane-info-notice'
function PdfLogsViewer() { function PdfLogsViewer() {
const { const {
@ -27,19 +27,9 @@ function PdfLogsViewer() {
return ( return (
<div className="logs-pane"> <div className="logs-pane">
<div className="logs-pane-content"> <div className="logs-pane-content">
<LogsPaneInfoNotice /> <PdfLogsPaneInfoNotice />
{codeCheckFailed && (
<div className="log-entry"> {codeCheckFailed && <PdfCodeCheckFailedNotice />}
<div className="log-entry-header log-entry-header-error">
<div className="log-entry-header-icon-container">
<Icon type="exclamation-triangle" modifier="fw" />
</div>
<h3 className="log-entry-header-title">
{t('code_check_failed_explanation')}
</h3>
</div>
</div>
)}
{error && <PdfPreviewError error={error} />} {error && <PdfPreviewError error={error} />}

View file

@ -0,0 +1,25 @@
import { memo } from 'react'
import { ButtonToolbar } from 'react-bootstrap'
import PdfCompileButton from './pdf-compile-button'
import PdfExpandButton from './pdf-expand-button'
import PdfHybridLogsButton from './pdf-hybrid-logs-button'
import PdfHybridDownloadButton from './pdf-hybrid-download-button'
import PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
function PdfPreviewHybridToolbar() {
return (
<ButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
<div className="toolbar-pdf-left">
<PdfCompileButton />
<PdfHybridLogsButton />
<PdfHybridDownloadButton />
</div>
<div className="toolbar-pdf-right">
<PdfHybridCodeCheckButton />
<PdfExpandButton />
</div>
</ButtonToolbar>
)
}
export default memo(PdfPreviewHybridToolbar)

View file

@ -2,15 +2,20 @@ import { memo, Suspense } from 'react'
import PdfLogsViewer from './pdf-logs-viewer' import PdfLogsViewer from './pdf-logs-viewer'
import PdfViewer from './pdf-viewer' import PdfViewer from './pdf-viewer'
import { usePdfPreviewContext } from '../contexts/pdf-preview-context' import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
import PdfPreviewToolbar from './pdf-preview-toolbar'
import LoadingSpinner from '../../../shared/components/loading-spinner' import LoadingSpinner from '../../../shared/components/loading-spinner'
import PdfHybridPreviewToolbar from './pdf-preview-hybrid-toolbar'
import PdfPreviewToolbar from './pdf-preview-toolbar'
const newPreviewToolbar = new URLSearchParams(window.location.search).has(
'new_preview_toolbar'
)
function PdfPreviewPane() { function PdfPreviewPane() {
const { showLogs } = usePdfPreviewContext() const { showLogs } = usePdfPreviewContext()
return ( return (
<div className="pdf full-size"> <div className="pdf full-size">
<PdfPreviewToolbar /> {newPreviewToolbar ? <PdfPreviewToolbar /> : <PdfHybridPreviewToolbar />}
<Suspense fallback={<LoadingPreview />}> <Suspense fallback={<LoadingPreview />}>
<div className="pdf-viewer"> <div className="pdf-viewer">
<PdfViewer /> <PdfViewer />

View file

@ -0,0 +1,22 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
function PdfCodeCheckFailedNotice() {
const { t } = useTranslation()
return (
<div className="log-entry">
<div className="log-entry-header log-entry-header-error">
<div className="log-entry-header-icon-container">
<Icon type="exclamation-triangle" modifier="fw" />
</div>
<h3 className="log-entry-header-title">
{t('code_check_failed_explanation')}
</h3>
</div>
</div>
)
}
export default memo(PdfCodeCheckFailedNotice)

View file

@ -0,0 +1,49 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import usePersistedState from '../../../shared/hooks/use-persisted-state'
function PdfLogsPaneInfoNotice() {
const { t } = useTranslation()
const [dismissed, setDismissed] = usePersistedState(
'logs_pane.dismissed_info_notice',
false
)
if (dismissed) {
return null
}
return (
<div className="log-entry">
<div className="log-entry-header log-entry-header-raw">
<div className="log-entry-header-icon-container">
<span className="info-badge" />
</div>
<h3 className="log-entry-header-title">
{t('logs_pane_info_message')}
</h3>
<a
href="https://forms.gle/zYByeRPcDtA6nDS19"
target="_blank"
rel="noopener noreferrer"
className="log-entry-header-link log-entry-header-link-raw"
>
<span className="log-entry-header-link-location">
{t('give_feedback')}
</span>
</a>
<button
className="btn-inline-link log-entry-header-link"
type="button"
aria-label={t('dismiss')}
onClick={() => setDismissed(true)}
>
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
)
}
export default memo(PdfLogsPaneInfoNotice)

View file

@ -78,6 +78,16 @@ function PreviewLogEntryContent({
return ( return (
<div className="log-entry-content"> <div className="log-entry-content">
{formattedContent ? (
<div className="log-entry-formatted-content">{formattedContent}</div>
) : null}
{extraInfoURL ? (
<div className="log-entry-content-link">
<a href={extraInfoURL} target="_blank" rel="noopener">
{t('log_hint_extra_info')}
</a>
</div>
) : null}
{rawContent ? ( {rawContent ? (
<div className="log-entry-content-raw-container"> <div className="log-entry-content-raw-container">
<div {...expandableProps}> <div {...expandableProps}>
@ -104,16 +114,6 @@ function PreviewLogEntryContent({
) : null} ) : null}
</div> </div>
) : null} ) : null}
{formattedContent ? (
<div className="log-entry-formatted-content">{formattedContent}</div>
) : null}
{extraInfoURL ? (
<div className="log-entry-content-link">
<a href={extraInfoURL} target="_blank" rel="noopener">
{t('log_hint_extra_info')}
</a>
</div>
) : null}
</div> </div>
) )
} }

View file

@ -14,6 +14,7 @@ import { buildFileList } from '../js/features/pdf-preview/util/file-list'
import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer' import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
import examplePdf from './fixtures/storybook-example.pdf' import examplePdf from './fixtures/storybook-example.pdf'
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error' import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
setupContext() setupContext()
@ -22,6 +23,7 @@ export default {
component: PdfPreview, component: PdfPreview,
subcomponents: { subcomponents: {
PdfPreviewToolbar, PdfPreviewToolbar,
PdfPreviewHybridToolbar,
PdfFileList, PdfFileList,
PdfPreviewError, PdfPreviewError,
}, },
@ -496,6 +498,22 @@ export const Toolbar = () => {
) )
} }
export const HybridToolbar = () => {
useFetchMock(fetchMock => {
mockCompile(fetchMock, 500)
mockBuildFile(fetchMock)
})
return withContextRoot(
<PdfPreviewProvider>
<div className="pdf">
<PdfPreviewHybridToolbar />
</div>
</PdfPreviewProvider>,
scope
)
}
export const FileList = () => { export const FileList = () => {
const fileList = useMemo(() => { const fileList = useMemo(() => {
return buildFileList(outputFiles) return buildFileList(outputFiles)

View file

@ -137,6 +137,7 @@
background-color: @ol-blue-gray-1; background-color: @ol-blue-gray-1;
border-radius: @border-radius-base; border-radius: @border-radius-base;
overflow: hidden; overflow: hidden;
margin-top: @margin-sm;
} }
.log-entry-content-raw { .log-entry-content-raw {

View file

@ -12,6 +12,7 @@
.toolbar-small-mixin; .toolbar-small-mixin;
.toolbar-alt-mixin; .toolbar-alt-mixin;
padding-right: 5px; padding-right: 5px;
margin-left: 0;
&.changes-to-autocompile { &.changes-to-autocompile {
// prettier-ignore // prettier-ignore
#gradient > .striped(@color: rgba(255, 255, 255, 0.1), @angle: -45deg); #gradient > .striped(@color: rgba(255, 255, 255, 0.1), @angle: -45deg);
@ -56,6 +57,61 @@
padding-left: @line-height-computed / 4; padding-left: @line-height-computed / 4;
} }
.toolbar-pdf-hybrid {
.btn:not(.btn-recompile) {
display: inline-block;
color: @toolbar-btn-color;
background-color: transparent;
padding: 4px 2px;
line-height: 1;
height: 24px;
border-radius: 2px;
&:hover,
&:active,
&:focus {
color: @toolbar-btn-color;
}
.label {
position: absolute;
top: 0;
right: 0;
padding: 0.15em 0.6em 0.2em;
font-size: 60%;
pointer-events: none;
}
.btn {
display: inline-block;
color: #fff;
background-color: transparent;
padding: 4px 2px;
line-height: 1;
height: 24px;
border-radius: 2px;
}
&.log-btn {
margin-right: 3px;
&.active {
color: white;
background-color: @link-color;
box-shadow: @toolbar-icon-btn-hover-boxshadow;
&:hover {
color: white;
}
}
&:focus {
outline: none;
}
}
}
}
.pdf { .pdf {
background-color: @pdf-bg; background-color: @pdf-bg;
} }

View file

@ -317,7 +317,7 @@ describe('<PdfPreview/>', function () {
await screen.findByRole('button', { name: 'Recompile' }) await screen.findByRole('button', { name: 'Recompile' })
const logsButton = screen.getByRole('button', { const logsButton = screen.getByRole('button', {
name: 'This project has an error', name: 'View logs',
}) })
logsButton.click() logsButton.click()
@ -350,7 +350,7 @@ describe('<PdfPreview/>', function () {
// show the logs UI // show the logs UI
const logsButton = screen.getByRole('button', { const logsButton = screen.getByRole('button', {
name: 'This project has an error', name: 'View logs',
}) })
logsButton.click() logsButton.click()