diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 5016651119..77e7a44af6 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -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, diff --git a/services/web/app/views/project/editor/left-menu-react.pug b/services/web/app/views/project/editor/left-menu-react.pug new file mode 100644 index 0000000000..d6376250a6 --- /dev/null +++ b/services/web/app/views/project/editor/left-menu-react.pug @@ -0,0 +1 @@ +editor-left-menu() \ No newline at end of file diff --git a/services/web/app/views/project/editor/main.pug b/services/web/app/views/project/editor/main.pug index 5d118ad5ea..d8b2df79b9 100644 --- a/services/web/app/views/project/editor/main.pug +++ b/services/web/app/views/project/editor/main.pug @@ -1,4 +1,7 @@ -include ./left-menu +if editorLeftMenuReact + include ./left-menu-react +else + include ./left-menu #chat-wrapper.full-size( layout="chat", diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 46a713e0c9..a1f90a936f 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/editor-left-menu/components/download-menu.tsx b/services/web/frontend/js/features/editor-left-menu/components/download-menu.tsx new file mode 100644 index 0000000000..e0bb6c453e --- /dev/null +++ b/services/web/frontend/js/features/editor-left-menu/components/download-menu.tsx @@ -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 ( + <> +

{t('download')}

+ + + ) +} diff --git a/services/web/frontend/js/features/editor-left-menu/components/download-pdf.tsx b/services/web/frontend/js/features/editor-left-menu/components/download-pdf.tsx new file mode 100644 index 0000000000..2feb281d29 --- /dev/null +++ b/services/web/frontend/js/features/editor-left-menu/components/download-pdf.tsx @@ -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 ( + + +
+ PDF +
+ ) + } else { + return ( + +
+ +
+ PDF +
+
+ ) + } +} diff --git a/services/web/frontend/js/features/editor-left-menu/components/download-source.tsx b/services/web/frontend/js/features/editor-left-menu/components/download-source.tsx new file mode 100644 index 0000000000..fe1c7ccafb --- /dev/null +++ b/services/web/frontend/js/features/editor-left-menu/components/download-source.tsx @@ -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 ( + + +
+ {t('source')} +
+ ) +} diff --git a/services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx b/services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx new file mode 100644 index 0000000000..235a6a9205 --- /dev/null +++ b/services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx @@ -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 ( + <> + + {leftMenuShown ? ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions +
setLeftMenuShown(false)} /> + ) : null} + + ) +} diff --git a/services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js b/services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js new file mode 100644 index 0000000000..64afa8ca66 --- /dev/null +++ b/services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js @@ -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))) diff --git a/services/web/frontend/js/ide/settings/index.js b/services/web/frontend/js/ide/settings/index.js index ed0c5699e7..b0459b687c 100644 --- a/services/web/frontend/js/ide/settings/index.js +++ b/services/web/frontend/js/ide/settings/index.js @@ -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' diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 150b6efa58..f17d570e33 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -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 => { - mockCompile(fetchMock) - mockCompileError(fetchMock) - mockBuildFile(fetchMock) + if (opts.mockCompileOnLoad) { + mockCompile(fetchMock) + mockCompileError(fetchMock) + mockBuildFile(fetchMock) + } }) // clear scopeWatchers on unmount diff --git a/services/web/frontend/stories/editor-left-menu/download-menu.stories.tsx b/services/web/frontend/stories/editor-left-menu/download-menu.stories.tsx new file mode 100644 index 0000000000..c13926ee03 --- /dev/null +++ b/services/web/frontend/stories/editor-left-menu/download-menu.stories.tsx @@ -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 ( +
+ +
+ ) +} + +export const CompileSuccess = () => { + useScope({ + editor: { + sharejs_doc: mockDocument(document.tex), + }, + }) + + useFetchMock(fetchMock => { + mockCompile(fetchMock) + }) + + return ( +
+ +
+ ) +} diff --git a/services/web/frontend/stories/fixtures/document.ts b/services/web/frontend/stories/fixtures/document.ts new file mode 100644 index 0000000000..2ff0466a72 --- /dev/null +++ b/services/web/frontend/stories/fixtures/document.ts @@ -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}.`, +} diff --git a/services/web/test/frontend/features/editor-left-menu/components/download-menu.test.js b/services/web/test/frontend/features/editor-left-menu/components/download-menu.test.js new file mode 100644 index 0000000000..3bd9d2f9cb --- /dev/null +++ b/services/web/test/frontend/features/editor-left-menu/components/download-menu.test.js @@ -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('', 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(, { + 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' + ) + }) + }) +})