mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-17 06:17:40 +00:00
Merge pull request #3282 from overleaf/jel-download-dropdown-menu
Add output download dropdown GitOrigin-RevId: 1f97f0d681679268d28796fbc8251ffbc6e98433
This commit is contained in:
parent
f8a8c9bbd6
commit
6fe36b6acb
9 changed files with 289 additions and 2 deletions
services/web
app/views/project/editor
frontend
extracted-translation-keys.json
js
features/preview/components
ide/pdf/controllers
stylesheets/app/editor
locales
test/frontend/features/preview/components
|
@ -16,6 +16,8 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
on-set-draft-mode="setDraftMode"
|
||||
on-set-syntax-check="setSyntaxCheck"
|
||||
on-toggle-logs="toggleLogs"
|
||||
output-files="pdf.outputFiles"
|
||||
pdf-download-url="pdf.downloadUrl"
|
||||
show-logs="shouldShowLogs"
|
||||
)
|
||||
else
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
[
|
||||
"download_pdf",
|
||||
"download_file",
|
||||
"n_warnings",
|
||||
"n_warnings_plural",
|
||||
"n_errors",
|
||||
|
@ -16,6 +18,7 @@
|
|||
"auto_compile",
|
||||
"on",
|
||||
"off",
|
||||
"other_output_files",
|
||||
"compile_mode",
|
||||
"normal",
|
||||
"fast",
|
||||
|
@ -25,6 +28,7 @@
|
|||
"loading",
|
||||
"no_messages",
|
||||
"send_first_message",
|
||||
"toggle_output_files_list",
|
||||
"your_message",
|
||||
"your_project_has_errors",
|
||||
"view_warnings",
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dropdown, MenuItem } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
export const topFileTypes = ['bbl', 'gls', 'ind']
|
||||
|
||||
function PreviewDownloadButton({ isCompiling, outputFiles, pdfDownloadUrl }) {
|
||||
let topFiles = []
|
||||
let otherFiles = []
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (outputFiles) {
|
||||
topFiles = outputFiles.filter(file => {
|
||||
if (topFileTypes.includes(file.type)) {
|
||||
return file
|
||||
}
|
||||
})
|
||||
|
||||
otherFiles = outputFiles.filter(file => {
|
||||
if (!topFileTypes.includes(file.type)) {
|
||||
if (file.type === 'pdf' && file.main === true) return
|
||||
return file
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown id="download-dropdown" disabled={isCompiling}>
|
||||
<a
|
||||
className="btn btn-xs btn-info"
|
||||
disabled={isCompiling || !pdfDownloadUrl}
|
||||
download
|
||||
href={pdfDownloadUrl || '#'}
|
||||
>
|
||||
<Icon type="download" modifier="fw" /> {t('download_pdf')}
|
||||
</a>
|
||||
<Dropdown.Toggle
|
||||
className="btn btn-xs btn-info dropdown-toggle"
|
||||
aria-label={t('toggle_output_files_list')}
|
||||
/>
|
||||
<Dropdown.Menu id="download-dropdown-list">
|
||||
<FileList list={topFiles} listType="main" />
|
||||
|
||||
{otherFiles.length > 0 && (
|
||||
<>
|
||||
<MenuItem divider />
|
||||
<MenuItem header>{t('other_output_files')}</MenuItem>
|
||||
<FileList list={otherFiles} listType="other" />
|
||||
</>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function FileList({ listType, list }) {
|
||||
return list.map((file, index) => {
|
||||
return (
|
||||
<MenuItem download href={file.url} key={`${listType}-${index}`}>
|
||||
<Trans
|
||||
i18nKey="download_file"
|
||||
components={[<strong />]}
|
||||
values={{ type: file.fileName }}
|
||||
/>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
PreviewDownloadButton.propTypes = {
|
||||
isCompiling: PropTypes.bool.isRequired,
|
||||
outputFiles: PropTypes.array,
|
||||
pdfDownloadUrl: PropTypes.string
|
||||
}
|
||||
|
||||
FileList.propTypes = {
|
||||
list: PropTypes.array.isRequired,
|
||||
listType: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default PreviewDownloadButton
|
|
@ -13,6 +13,8 @@ function PreviewPane({
|
|||
onSetDraftMode,
|
||||
onSetSyntaxCheck,
|
||||
onToggleLogs,
|
||||
outputFiles,
|
||||
pdfDownloadUrl,
|
||||
showLogs
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
@ -43,6 +45,8 @@ function PreviewPane({
|
|||
onSetDraftMode={onSetDraftMode}
|
||||
onSetSyntaxCheck={onSetSyntaxCheck}
|
||||
onToggleLogs={onToggleLogs}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
/>
|
||||
<span aria-live="polite" className="sr-only">
|
||||
{nErrors && !compilerState.isCompiling
|
||||
|
@ -76,6 +80,8 @@ PreviewPane.propTypes = {
|
|||
onSetDraftMode: PropTypes.func.isRequired,
|
||||
onSetSyntaxCheck: PropTypes.func.isRequired,
|
||||
onToggleLogs: PropTypes.func.isRequired,
|
||||
outputFiles: PropTypes.array,
|
||||
pdfDownloadUrl: PropTypes.string,
|
||||
showLogs: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import PreviewDownloadButton from './preview-download-button'
|
||||
import PreviewRecompileButton from './preview-recompile-button'
|
||||
import PreviewLogsToggleButton from './preview-logs-toggle-button'
|
||||
|
||||
|
@ -13,6 +14,8 @@ function PreviewToolbar({
|
|||
onSetDraftMode,
|
||||
onSetSyntaxCheck,
|
||||
onToggleLogs,
|
||||
outputFiles,
|
||||
pdfDownloadUrl,
|
||||
showLogs
|
||||
}) {
|
||||
return (
|
||||
|
@ -27,6 +30,11 @@ function PreviewToolbar({
|
|||
onSetSyntaxCheck={onSetSyntaxCheck}
|
||||
onClearCache={onClearCache}
|
||||
/>
|
||||
<PreviewDownloadButton
|
||||
isCompiling={compilerState.isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="toolbar-pdf-right">
|
||||
<PreviewLogsToggleButton
|
||||
|
@ -59,7 +67,9 @@ PreviewToolbar.propTypes = {
|
|||
onSetAutoCompile: PropTypes.func.isRequired,
|
||||
onSetDraftMode: PropTypes.func.isRequired,
|
||||
onSetSyntaxCheck: PropTypes.func.isRequired,
|
||||
onToggleLogs: PropTypes.func.isRequired
|
||||
onToggleLogs: PropTypes.func.isRequired,
|
||||
pdfDownloadUrl: PropTypes.string,
|
||||
outputFiles: PropTypes.array
|
||||
}
|
||||
|
||||
export default PreviewToolbar
|
||||
|
|
|
@ -508,7 +508,9 @@ App.controller('PdfController', function(
|
|||
url:
|
||||
`/project/${$scope.project_id}/output/${file.path}` +
|
||||
createQueryString(qs),
|
||||
main: !!isOutputFile
|
||||
main: !!isOutputFile,
|
||||
fileName: file.path,
|
||||
type: file.type
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -460,3 +460,10 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#download-dropdown-list {
|
||||
max-height: calc(
|
||||
~'100vh - ' @toolbar-small-height ~' - ' @toolbar-height ~' - ' @margin-md
|
||||
);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"other_output_files": "Other output files",
|
||||
"toggle_output_files_list": "Toggle output files list",
|
||||
"n_warnings": "__count__ warning",
|
||||
"n_warnings_plural": "__count__ warnings",
|
||||
"n_errors": "__count__ error",
|
||||
|
@ -1106,6 +1108,7 @@
|
|||
"remove_collaborator": "Remove collaborator",
|
||||
"add_to_folders": "Add to folders",
|
||||
"download_zip_file": "Download .zip File",
|
||||
"download_file": "Download <0>__type__</0> file",
|
||||
"price": "Price",
|
||||
"close": "Close",
|
||||
"keybindings": "Keybindings",
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
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'
|
||||
|
||||
describe('<PreviewDownloadButton />', 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
|
||||
}
|
||||
}
|
||||
|
||||
it('should disable the button and dropdown toggle when compiling', function() {
|
||||
const isCompiling = true
|
||||
const outputFiles = undefined
|
||||
render(
|
||||
<PreviewDownloadButton
|
||||
isCompiling={isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={undefined}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('Download PDF').getAttribute('disabled')).to.exist
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).to.equal(1) // the dropdown toggle
|
||||
expect(buttons[0].getAttribute('disabled')).to.exist
|
||||
expect(buttons[0].getAttribute('aria-label')).to.equal(
|
||||
'Toggle output files list'
|
||||
)
|
||||
})
|
||||
it('should disable the PDF button when there is no PDF', function() {
|
||||
const isCompiling = false
|
||||
const outputFiles = []
|
||||
render(
|
||||
<PreviewDownloadButton
|
||||
isCompiling={isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={undefined}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('Download PDF').getAttribute('disabled')).to.exist
|
||||
})
|
||||
it('should enable the PDF button when there is a main PDF', function() {
|
||||
const isCompiling = false
|
||||
const outputFiles = []
|
||||
render(
|
||||
<PreviewDownloadButton
|
||||
isCompiling={isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('Download PDF').getAttribute('href')).to.equal(
|
||||
pdfDownloadUrl
|
||||
)
|
||||
expect(screen.getByText('Download PDF').getAttribute('disabled')).to.not
|
||||
.exist
|
||||
})
|
||||
it('should enable the dropdown when not compiling', function() {
|
||||
const isCompiling = false
|
||||
const outputFiles = []
|
||||
render(
|
||||
<PreviewDownloadButton
|
||||
isCompiling={isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
/>
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
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')
|
||||
]
|
||||
|
||||
render(
|
||||
<PreviewDownloadButton
|
||||
isCompiling={isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={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 itemTextParts = item.textContent.split(' ')
|
||||
expect(itemTextParts[0]).to.equal('Download')
|
||||
const fileType = itemTextParts[1].split('.').pop()
|
||||
expect(fileTypes).to.include(fileType)
|
||||
expect(itemTextParts[2]).to.equal('file')
|
||||
})
|
||||
|
||||
// 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]
|
||||
render(
|
||||
<PreviewDownloadButton
|
||||
isCompiling={isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
/>
|
||||
)
|
||||
const bblMenuItems = screen.getAllByText((content, element) => {
|
||||
return (
|
||||
content !== '' && element.textContent === 'Download output.bbl file'
|
||||
)
|
||||
})
|
||||
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]
|
||||
|
||||
render(
|
||||
<PreviewDownloadButton
|
||||
isCompiling={isCompiling}
|
||||
outputFiles={outputFiles}
|
||||
pdfDownloadUrl={pdfDownloadUrl}
|
||||
/>
|
||||
)
|
||||
screen.getAllByRole('menuitem', { name: 'Download alt.pdf file' })
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue