mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Add hybrid toolbar to migrated PDF preview (#5414)
GitOrigin-RevId: 6266028091229c819aee3c8d4bd3bff2e2417125
This commit is contained in:
parent
7f7e5ed749
commit
e26d47cb41
15 changed files with 364 additions and 43 deletions
|
@ -55,8 +55,8 @@
|
|||
"conflicting_paths_found": "",
|
||||
"connected_users": "",
|
||||
"contact_message_label": "",
|
||||
"continue_github_merge": "",
|
||||
"contact_us": "",
|
||||
"continue_github_merge": "",
|
||||
"copy": "",
|
||||
"copy_project": "",
|
||||
"copying": "",
|
||||
|
@ -179,9 +179,10 @@
|
|||
"log_entry_maximum_entries_message": "",
|
||||
"log_entry_maximum_entries_title": "",
|
||||
"log_hint_extra_info": "",
|
||||
"log_viewer_error": "",
|
||||
"logs_and_output_files": "",
|
||||
"logs_pane_info_message": "",
|
||||
"logs_pane_info_message_popup": "",
|
||||
"log_viewer_error": "",
|
||||
"main_file_not_found": "",
|
||||
"make_private": "",
|
||||
"manage_files_from_your_dropbox_folder": "",
|
||||
|
@ -257,7 +258,7 @@
|
|||
"project_too_large": "",
|
||||
"project_too_large_please_reduce": "",
|
||||
"project_too_much_editable_text": "",
|
||||
"project_url" : "",
|
||||
"project_url": "",
|
||||
"public": "",
|
||||
"pull_github_changes_into_sharelatex": "",
|
||||
"push_sharelatex_changes_to_github": "",
|
||||
|
@ -269,6 +270,7 @@
|
|||
"recent_commits_in_github": "",
|
||||
"recompile": "",
|
||||
"recompile_from_scratch": "",
|
||||
"recompile_pdf": "",
|
||||
"reconnect": "",
|
||||
"reference_error_relink_hint": "",
|
||||
"refresh": "",
|
||||
|
|
|
@ -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 ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
@ -6,6 +12,8 @@ import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
|
|||
import { memo } from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
|
||||
|
||||
function PdfCompileButton() {
|
||||
const {
|
||||
autoCompile,
|
||||
|
@ -33,6 +41,16 @@ function PdfCompileButton() {
|
|||
'btn-recompile-group-has-changes': hasChanges,
|
||||
})}
|
||||
id="pdf-recompile-dropdown"
|
||||
>
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
delayShow={500}
|
||||
overlay={
|
||||
<Tooltip id="tooltip-logs-toggle" className="keyboard-tooltip">
|
||||
{t('recompile_pdf')}{' '}
|
||||
<span className="keyboard-shortcut">({modifierKey} + Enter)</span>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="btn-recompile"
|
||||
|
@ -41,10 +59,11 @@ function PdfCompileButton() {
|
|||
aria-label={compileButtonLabel}
|
||||
>
|
||||
<Icon type="refresh" spin={compiling} />
|
||||
<span className="toolbar-text toolbar-hide-medium toolbar-hide-small">
|
||||
<span className="toolbar-hide-medium toolbar-hide-small btn-recompile-label">
|
||||
{compileButtonLabel}
|
||||
</span>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
|
||||
<Dropdown.Toggle
|
||||
aria-label={t('toggle_compile_options_menu')}
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -1,4 +1,3 @@
|
|||
import Icon from '../../../shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry'
|
||||
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 withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
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() {
|
||||
const {
|
||||
|
@ -27,19 +27,9 @@ function PdfLogsViewer() {
|
|||
return (
|
||||
<div className="logs-pane">
|
||||
<div className="logs-pane-content">
|
||||
<LogsPaneInfoNotice />
|
||||
{codeCheckFailed && (
|
||||
<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>
|
||||
)}
|
||||
<PdfLogsPaneInfoNotice />
|
||||
|
||||
{codeCheckFailed && <PdfCodeCheckFailedNotice />}
|
||||
|
||||
{error && <PdfPreviewError error={error} />}
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -2,15 +2,20 @@ import { memo, Suspense } from 'react'
|
|||
import PdfLogsViewer from './pdf-logs-viewer'
|
||||
import PdfViewer from './pdf-viewer'
|
||||
import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
|
||||
import PdfPreviewToolbar from './pdf-preview-toolbar'
|
||||
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() {
|
||||
const { showLogs } = usePdfPreviewContext()
|
||||
|
||||
return (
|
||||
<div className="pdf full-size">
|
||||
<PdfPreviewToolbar />
|
||||
{newPreviewToolbar ? <PdfPreviewToolbar /> : <PdfHybridPreviewToolbar />}
|
||||
<Suspense fallback={<LoadingPreview />}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfViewer />
|
||||
|
|
|
@ -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)
|
|
@ -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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PdfLogsPaneInfoNotice)
|
|
@ -78,6 +78,16 @@ function PreviewLogEntryContent({
|
|||
|
||||
return (
|
||||
<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 ? (
|
||||
<div className="log-entry-content-raw-container">
|
||||
<div {...expandableProps}>
|
||||
|
@ -104,16 +114,6 @@ function PreviewLogEntryContent({
|
|||
) : null}
|
||||
</div>
|
||||
) : 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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 examplePdf from './fixtures/storybook-example.pdf'
|
||||
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
|
||||
import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
||||
|
||||
setupContext()
|
||||
|
||||
|
@ -22,6 +23,7 @@ export default {
|
|||
component: PdfPreview,
|
||||
subcomponents: {
|
||||
PdfPreviewToolbar,
|
||||
PdfPreviewHybridToolbar,
|
||||
PdfFileList,
|
||||
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 = () => {
|
||||
const fileList = useMemo(() => {
|
||||
return buildFileList(outputFiles)
|
||||
|
|
|
@ -137,6 +137,7 @@
|
|||
background-color: @ol-blue-gray-1;
|
||||
border-radius: @border-radius-base;
|
||||
overflow: hidden;
|
||||
margin-top: @margin-sm;
|
||||
}
|
||||
|
||||
.log-entry-content-raw {
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
.toolbar-small-mixin;
|
||||
.toolbar-alt-mixin;
|
||||
padding-right: 5px;
|
||||
margin-left: 0;
|
||||
&.changes-to-autocompile {
|
||||
// prettier-ignore
|
||||
#gradient > .striped(@color: rgba(255, 255, 255, 0.1), @angle: -45deg);
|
||||
|
@ -56,6 +57,61 @@
|
|||
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 {
|
||||
background-color: @pdf-bg;
|
||||
}
|
||||
|
|
|
@ -317,7 +317,7 @@ describe('<PdfPreview/>', function () {
|
|||
await screen.findByRole('button', { name: 'Recompile' })
|
||||
|
||||
const logsButton = screen.getByRole('button', {
|
||||
name: 'This project has an error',
|
||||
name: 'View logs',
|
||||
})
|
||||
logsButton.click()
|
||||
|
||||
|
@ -350,7 +350,7 @@ describe('<PdfPreview/>', function () {
|
|||
|
||||
// show the logs UI
|
||||
const logsButton = screen.getByRole('button', {
|
||||
name: 'This project has an error',
|
||||
name: 'View logs',
|
||||
})
|
||||
logsButton.click()
|
||||
|
||||
|
|
Loading…
Reference in a new issue