From 6fe36b6acbd472a64ebc84cd639f59009a9c52b7 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 20 Oct 2020 07:44:32 -0500 Subject: [PATCH] Merge pull request #3282 from overleaf/jel-download-dropdown-menu Add output download dropdown GitOrigin-RevId: 1f97f0d681679268d28796fbc8251ffbc6e98433 --- services/web/app/views/project/editor/pdf.pug | 2 + .../frontend/extracted-translation-keys.json | 4 + .../components/preview-download-button.js | 83 +++++++++ .../preview/components/preview-pane.js | 6 + .../preview/components/preview-toolbar.js | 12 +- .../js/ide/pdf/controllers/PdfController.js | 4 +- .../frontend/stylesheets/app/editor/pdf.less | 7 + services/web/locales/en.json | 3 + .../preview-download-button.test.js | 170 ++++++++++++++++++ 9 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 services/web/frontend/js/features/preview/components/preview-download-button.js create mode 100644 services/web/test/frontend/features/preview/components/preview-download-button.test.js 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 && ( + <> + + {t('other_output_files')} + + + )} + + + ) +} + +function FileList({ listType, list }) { + return list.map((file, index) => { + return ( + + ]} + values={{ type: file.fileName }} + /> + + ) + }) +} + +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__ 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' }) + }) +})