diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug
index aeffef7617..acedba7bc3 100644
--- a/services/web/app/views/project/editor/pdf.pug
+++ b/services/web/app/views/project/editor/pdf.pug
@@ -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
diff --git a/services/web/frontend/extracted-translation-keys.json b/services/web/frontend/extracted-translation-keys.json
index b0554a1f42..8381c67561 100644
--- a/services/web/frontend/extracted-translation-keys.json
+++ b/services/web/frontend/extracted-translation-keys.json
@@ -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",
diff --git a/services/web/frontend/js/features/preview/components/preview-download-button.js b/services/web/frontend/js/features/preview/components/preview-download-button.js
new file mode 100644
index 0000000000..6a0f7d9516
--- /dev/null
+++ b/services/web/frontend/js/features/preview/components/preview-download-button.js
@@ -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 (
+
+
+ {t('download_pdf')}
+
+
+
+
+
+ {otherFiles.length > 0 && (
+ <>
+
+
+
+ >
+ )}
+
+
+ )
+}
+
+function FileList({ listType, list }) {
+ return list.map((file, index) => {
+ return (
+
+ )
+ })
+}
+
+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
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 795461a8fc..5ec3511cc5 100644
--- a/services/web/frontend/js/features/preview/components/preview-pane.js
+++ b/services/web/frontend/js/features/preview/components/preview-pane.js
@@ -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}
/>
{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
}
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 b4ae5cf33c..67b9af5fc9 100644
--- a/services/web/frontend/js/features/preview/components/preview-toolbar.js
+++ b/services/web/frontend/js/features/preview/components/preview-toolbar.js
@@ -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}
/>
+
__type__0> file",
"price": "Price",
"close": "Close",
"keybindings": "Keybindings",
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
new file mode 100644
index 0000000000..5043343129
--- /dev/null
+++ b/services/web/test/frontend/features/preview/components/preview-download-button.test.js
@@ -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('', 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+
+ 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(
+
+ )
+ 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(
+
+ )
+ screen.getAllByRole('menuitem', { name: 'Download alt.pdf file' })
+ })
+})