mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Migrate download menu in editor left menu to react (#10046)
* Initialize left menu react migration and migration download menu UI to react * Add test case to DownloadMenu react component * Update test description and add an href check to one of the download link * Extract storybook document mock to its own fixture file * Add mockCompileOnLoad config on storybook editor scope - if mockCompileOnLoad: true (default), then the default compile mock will be used - If mockCompileOnLoad: false, then we have to provide a compile mock on the storybook component * Create download menu storybook component * Use a single "editor-left-menu" controller on the editor left menu migrations * Remove the form import from the react version of the left menu * Change inline style to utility class name GitOrigin-RevId: 5357c7bfc78bf40f52b9b308df8f2b60d793fbf7
This commit is contained in:
parent
58ba814cd1
commit
797b9b2532
14 changed files with 283 additions and 5 deletions
|
@ -1041,6 +1041,21 @@ const ProjectController = {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
editorLeftMenuAssignment(cb) {
|
||||||
|
SplitTestHandler.getAssignment(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
'editor-left-menu',
|
||||||
|
(error, assignment) => {
|
||||||
|
// do not fail editor load if assignment fails
|
||||||
|
if (error) {
|
||||||
|
cb(null, { variant: 'default' })
|
||||||
|
} else {
|
||||||
|
cb(null, assignment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
(
|
(
|
||||||
err,
|
err,
|
||||||
|
@ -1057,6 +1072,7 @@ const ProjectController = {
|
||||||
newSourceEditorAssignment,
|
newSourceEditorAssignment,
|
||||||
pdfjsAssignment,
|
pdfjsAssignment,
|
||||||
dictionaryEditorAssignment,
|
dictionaryEditorAssignment,
|
||||||
|
editorLeftMenuAssignment,
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
|
@ -1138,6 +1154,9 @@ const ProjectController = {
|
||||||
user.betaProgram ||
|
user.betaProgram ||
|
||||||
shouldDisplayFeature('new_source_editor', false) // also allow override via ?new_source_editor=true
|
shouldDisplayFeature('new_source_editor', false) // also allow override via ?new_source_editor=true
|
||||||
|
|
||||||
|
const editorLeftMenuReact =
|
||||||
|
editorLeftMenuAssignment?.variant === 'react'
|
||||||
|
|
||||||
const showSymbolPalette =
|
const showSymbolPalette =
|
||||||
!Features.hasFeature('saas') ||
|
!Features.hasFeature('saas') ||
|
||||||
(user.features && user.features.symbolPalette)
|
(user.features && user.features.symbolPalette)
|
||||||
|
@ -1173,6 +1192,7 @@ const ProjectController = {
|
||||||
bodyClasses: ['editor'],
|
bodyClasses: ['editor'],
|
||||||
project_id: project._id,
|
project_id: project._id,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
|
editorLeftMenuReact,
|
||||||
user: {
|
user: {
|
||||||
id: userId,
|
id: userId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
editor-left-menu()
|
|
@ -1,3 +1,6 @@
|
||||||
|
if editorLeftMenuReact
|
||||||
|
include ./left-menu-react
|
||||||
|
else
|
||||||
include ./left-menu
|
include ./left-menu
|
||||||
|
|
||||||
#chat-wrapper.full-size(
|
#chat-wrapper.full-size(
|
||||||
|
|
|
@ -593,6 +593,7 @@
|
||||||
"somthing_went_wrong_compiling": "",
|
"somthing_went_wrong_compiling": "",
|
||||||
"sort_by": "",
|
"sort_by": "",
|
||||||
"sort_by_x": "",
|
"sort_by_x": "",
|
||||||
|
"source": "",
|
||||||
"sso_link_error": "",
|
"sso_link_error": "",
|
||||||
"start_by_adding_your_email": "",
|
"start_by_adding_your_email": "",
|
||||||
"start_free_trial": "",
|
"start_free_trial": "",
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import DownloadPDF from './download-pdf'
|
||||||
|
import DownloadSource from './download-source'
|
||||||
|
|
||||||
|
export default function DownloadMenu() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="mt-0">{t('download')}</h4>
|
||||||
|
<ul className="list-unstyled nav nav-downloads text-center">
|
||||||
|
<li>
|
||||||
|
<DownloadSource />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<DownloadPDF />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
import Tooltip from '../../../shared/components/tooltip'
|
||||||
|
|
||||||
|
export default function DownloadPDF() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
|
||||||
|
|
||||||
|
if (pdfUrl) {
|
||||||
|
return (
|
||||||
|
<a href={pdfDownloadUrl || pdfUrl} target="_blank" rel="noreferrer">
|
||||||
|
<Icon type="file-pdf-o" modifier="2x" />
|
||||||
|
<br />
|
||||||
|
PDF
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
id="disabled-pdf-download"
|
||||||
|
description={t('please_compile_pdf_before_download')}
|
||||||
|
overlayProps={{ placement: 'bottom' }}
|
||||||
|
>
|
||||||
|
<div className="link-disabled">
|
||||||
|
<Icon type="file-pdf-o" modifier="2x" />
|
||||||
|
<br />
|
||||||
|
PDF
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useProjectContext } from '../../../shared/context/project-context'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
|
||||||
|
export default function DownloadSource() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { _id: projectId } = useProjectContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/project/${projectId}/download/zip`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<Icon type="file-archive-o" modifier="2x" />
|
||||||
|
<br />
|
||||||
|
{t('source')}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import DownloadMenu from './download-menu'
|
||||||
|
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
export default function EditorLeftMenu() {
|
||||||
|
const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<aside
|
||||||
|
id="left-menu"
|
||||||
|
className={classNames('full-size', { shown: leftMenuShown })}
|
||||||
|
>
|
||||||
|
<DownloadMenu />
|
||||||
|
</aside>
|
||||||
|
{leftMenuShown ? (
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
|
<div id="left-menu-mask" onClick={() => setLeftMenuShown(false)} />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import App from '../../../base'
|
||||||
|
import { react2angular } from 'react2angular'
|
||||||
|
import { rootContext } from '../../../shared/context/root-context'
|
||||||
|
import EditorLeftMenu from '../components/editor-left-menu'
|
||||||
|
|
||||||
|
App.component('editorLeftMenu', react2angular(rootContext.use(EditorLeftMenu)))
|
|
@ -3,3 +3,4 @@
|
||||||
import './services/settings'
|
import './services/settings'
|
||||||
import './controllers/SettingsController'
|
import './controllers/SettingsController'
|
||||||
import '../../features/dictionary/controllers/modal-controller'
|
import '../../features/dictionary/controllers/modal-controller'
|
||||||
|
import '../../features/editor-left-menu/controllers/editor-left-menu-controller'
|
||||||
|
|
|
@ -166,12 +166,21 @@ const initialize = () => {
|
||||||
return ide
|
return ide
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScopeDecorator = (Story: any) => {
|
type ScopeDecoratorOptions = {
|
||||||
|
mockCompileOnLoad: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScopeDecorator = (
|
||||||
|
Story: any,
|
||||||
|
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true }
|
||||||
|
) => {
|
||||||
// mock compile on load
|
// mock compile on load
|
||||||
useFetchMock(fetchMock => {
|
useFetchMock(fetchMock => {
|
||||||
|
if (opts.mockCompileOnLoad) {
|
||||||
mockCompile(fetchMock)
|
mockCompile(fetchMock)
|
||||||
mockCompileError(fetchMock)
|
mockCompileError(fetchMock)
|
||||||
mockBuildFile(fetchMock)
|
mockBuildFile(fetchMock)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// clear scopeWatchers on unmount
|
// clear scopeWatchers on unmount
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import DownloadMenu from '../../js/features/editor-left-menu/components/download-menu'
|
||||||
|
import { ScopeDecorator } from '../decorators/scope'
|
||||||
|
import { mockCompile, mockCompileError } from '../fixtures/compile'
|
||||||
|
import { document, mockDocument } from '../fixtures/document'
|
||||||
|
import useFetchMock from '../hooks/use-fetch-mock'
|
||||||
|
import { useScope } from '../hooks/use-scope'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Editor / Left Menu / Download Menu',
|
||||||
|
component: DownloadMenu,
|
||||||
|
decorators: [
|
||||||
|
(Story: any) => ScopeDecorator(Story, { mockCompileOnLoad: false }),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotCompiled = () => {
|
||||||
|
useFetchMock(fetchMock => {
|
||||||
|
mockCompileError(fetchMock, 'failure')
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="left-menu" className="shown">
|
||||||
|
<DownloadMenu />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompileSuccess = () => {
|
||||||
|
useScope({
|
||||||
|
editor: {
|
||||||
|
sharejs_doc: mockDocument(document.tex),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useFetchMock(fetchMock => {
|
||||||
|
mockCompile(fetchMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="left-menu" className="shown">
|
||||||
|
<DownloadMenu />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
39
services/web/frontend/stories/fixtures/document.ts
Normal file
39
services/web/frontend/stories/fixtures/document.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
export function mockDocument(text: string) {
|
||||||
|
return {
|
||||||
|
doc_id: 'story-doc',
|
||||||
|
getSnapshot: () => text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const document = {
|
||||||
|
tex: `\\documentclass{article}
|
||||||
|
|
||||||
|
% Language setting
|
||||||
|
% Replace \`english' with e.g. \`spanish' to change the document language
|
||||||
|
\\usepackage[english]{babel}
|
||||||
|
|
||||||
|
% Set page size and margins
|
||||||
|
% Replace \`letterpaper' with\`a4paper' for UK/EU standard size
|
||||||
|
\\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry}
|
||||||
|
|
||||||
|
% Useful packages
|
||||||
|
\\usepackage{amsmath}
|
||||||
|
\\usepackage{graphicx}
|
||||||
|
\\usepackage[colorlinks=true, allcolors=blue]{hyperref}
|
||||||
|
|
||||||
|
\\title{Your Paper}
|
||||||
|
\\author{You}
|
||||||
|
|
||||||
|
\\begin{document}
|
||||||
|
\\maketitle
|
||||||
|
|
||||||
|
\\begin{abstract}
|
||||||
|
Your abstract.
|
||||||
|
\\end{abstract}
|
||||||
|
|
||||||
|
\\section{Introduction}
|
||||||
|
|
||||||
|
Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started.
|
||||||
|
|
||||||
|
Once you're familiar with the editor, you can find various project setting in the Overleaf menu, accessed via the button in the very top left of the editor. To view tutorials, user guides, and further documentation, please visit our \\href{https://www.overleaf.com/learn}{help library}, or head to our plans page to \\href{https://www.overleaf.com/user/subscription/plans}{choose your plan}.`,
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { screen, waitFor } from '@testing-library/dom'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import DownloadMenu from '../../../../../frontend/js/features/editor-left-menu/components/download-menu'
|
||||||
|
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||||
|
|
||||||
|
describe('<DownloadMenu />', function () {
|
||||||
|
afterEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows download links with correct url', async function () {
|
||||||
|
fetchMock.post('express:/project/:projectId/compile', {
|
||||||
|
clsiServerId: 'foo',
|
||||||
|
compileGroup: 'priority',
|
||||||
|
status: 'success',
|
||||||
|
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
|
||||||
|
outputFiles: [
|
||||||
|
{
|
||||||
|
path: 'output.pdf',
|
||||||
|
build: 'build-123',
|
||||||
|
url: '/build/build-123/output.pdf',
|
||||||
|
type: 'pdf',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
renderWithEditorContext(<DownloadMenu />, {
|
||||||
|
projectId: '123abc',
|
||||||
|
scope: {
|
||||||
|
editor: {
|
||||||
|
sharejs_doc: {
|
||||||
|
doc_id: 'test-doc',
|
||||||
|
getSnapshot: () => 'some doc content',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const sourceLink = screen.getByRole('link', {
|
||||||
|
name: 'Source',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(sourceLink.getAttribute('href')).to.equal(
|
||||||
|
'/project/123abc/download/zip'
|
||||||
|
)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const pdfLink = screen.getByRole('link', {
|
||||||
|
name: 'PDF',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(pdfLink.getAttribute('href')).to.equal(
|
||||||
|
'/download/project/123abc/build/build-123/output/output.pdf?compileGroup=priority&clsiserverid=foo&popupDownload=true'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue