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:
M Fahru 2022-10-24 12:21:36 -04:00 committed by Copybot
parent 58ba814cd1
commit 797b9b2532
14 changed files with 283 additions and 5 deletions

View file

@ -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,
@ -1057,6 +1072,7 @@ const ProjectController = {
newSourceEditorAssignment,
pdfjsAssignment,
dictionaryEditorAssignment,
editorLeftMenuAssignment,
}
) => {
if (err != null) {
@ -1138,6 +1154,9 @@ const ProjectController = {
user.betaProgram ||
shouldDisplayFeature('new_source_editor', false) // also allow override via ?new_source_editor=true
const editorLeftMenuReact =
editorLeftMenuAssignment?.variant === 'react'
const showSymbolPalette =
!Features.hasFeature('saas') ||
(user.features && user.features.symbolPalette)
@ -1173,6 +1192,7 @@ const ProjectController = {
bodyClasses: ['editor'],
project_id: project._id,
projectName: project.name,
editorLeftMenuReact,
user: {
id: userId,
email: user.email,

View file

@ -0,0 +1 @@
editor-left-menu()

View file

@ -1,4 +1,7 @@
include ./left-menu
if editorLeftMenuReact
include ./left-menu-react
else
include ./left-menu
#chat-wrapper.full-size(
layout="chat",

View file

@ -593,6 +593,7 @@
"somthing_went_wrong_compiling": "",
"sort_by": "",
"sort_by_x": "",
"source": "",
"sso_link_error": "",
"start_by_adding_your_email": "",
"start_free_trial": "",

View file

@ -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>
</>
)
}

View file

@ -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>
)
}
}

View file

@ -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>
)
}

View file

@ -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}
</>
)
}

View file

@ -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)))

View file

@ -3,3 +3,4 @@
import './services/settings'
import './controllers/SettingsController'
import '../../features/dictionary/controllers/modal-controller'
import '../../features/editor-left-menu/controllers/editor-left-menu-controller'

View file

@ -166,12 +166,21 @@ const initialize = () => {
return ide
}
export const ScopeDecorator = (Story: any) => {
type ScopeDecoratorOptions = {
mockCompileOnLoad: boolean
}
export const ScopeDecorator = (
Story: any,
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true }
) => {
// mock compile on load
useFetchMock(fetchMock => {
if (opts.mockCompileOnLoad) {
mockCompile(fetchMock)
mockCompileError(fetchMock)
mockBuildFile(fetchMock)
}
})
// clear scopeWatchers on unmount

View file

@ -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>
)
}

View 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}.`,
}

View file

@ -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'
)
})
})
})