From 6ce36a26063d45eba754903de38c820bd7a620a6 Mon Sep 17 00:00:00 2001 From: Davinder Singh Date: Thu, 4 Jun 2026 12:08:48 +0100 Subject: [PATCH] adding web changes of Export HTML (#34117) GitOrigin-RevId: 804c576faefebfc6683a0363b45372e66a43d8fc --- .../Downloads/ProjectDownloadsController.mjs | 1 + .../Features/Project/ProjectController.mjs | 1 + .../web/frontend/extracted-translations.json | 2 + .../toolbar/export-document-toasts.tsx | 18 ++++- .../export-project-with-conversion-button.tsx | 2 +- .../ide-react/components/toolbar/menu-bar.tsx | 1 + .../components/toolbar/project-title.tsx | 6 ++ .../ide-react/hooks/use-convert-project.ts | 2 +- services/web/locales/en.json | 2 + .../ProjectDownloadsController.test.mjs | 75 +++++++++++++++++++ 10 files changed, 107 insertions(+), 3 deletions(-) diff --git a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs index 34b135f080..128911b4d3 100644 --- a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs +++ b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs @@ -19,6 +19,7 @@ const { z, zz, parseReq } = Validation const SUPPORTED_CONVERSION_TYPES = new Map([ ['docx', 'docx'], ['markdown', 'zip'], + ['html', 'zip'], ]) const exportProjectConversionSchema = z.object({ diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index c9453201a2..9eddf0bef7 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -483,6 +483,7 @@ const _ProjectController = { 'export-docx', 'sharing-updates', 'export-markdown', + 'export-html', 'command-palette', 'overleaf-library', 'compile-timeout-cta', diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index f0455fc789..627649bf52 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -654,6 +654,7 @@ "expires_on": "", "explore_plans": "", "export_as_docx": "", + "export_as_html": "", "export_as_markdown": "", "export_csv": "", "export_project_to_github": "", @@ -916,6 +917,7 @@ "how_to_insert_images": "", "how_we_use_your_data": "", "how_we_use_your_data_explanation": "", + "html_export_feedback_message": "", "i_confirm_am_student": "", "i_want_to_add_a_po_number": "", "i_want_to_stay": "", diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/export-document-toasts.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/export-document-toasts.tsx index 8e8a16687d..05d9ab1ccd 100644 --- a/services/web/frontend/js/features/ide-react/components/toolbar/export-document-toasts.tsx +++ b/services/web/frontend/js/features/ide-react/components/toolbar/export-document-toasts.tsx @@ -71,6 +71,20 @@ const ExportDocumentSuccessToast = ({ data }: { data?: any }) => { ]} /> ) + } else if (type === 'html') { + return ( + , + ]} + /> + ) } else { return ( { ) } -export const showExportDocumentSuccess = (type: 'docx' | 'markdown') => { +export const showExportDocumentSuccess = ( + type: 'docx' | 'markdown' | 'html' +) => { window.dispatchEvent( new CustomEvent('ide:show-toast', { detail: { key: 'export-document:success', type }, diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/export-project-with-conversion-button.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/export-project-with-conversion-button.tsx index 32861813da..22651b74e9 100644 --- a/services/web/frontend/js/features/ide-react/components/toolbar/export-project-with-conversion-button.tsx +++ b/services/web/frontend/js/features/ide-react/components/toolbar/export-project-with-conversion-button.tsx @@ -9,7 +9,7 @@ import { useEditorManagerContext } from '@/features/ide-react/context/editor-man type ExportProjectWithConversionProps = { featureFlag?: string - conversionType: 'docx' | 'markdown' + conversionType: 'docx' | 'markdown' | 'html' label: string menuBarId: string } diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/menu-bar.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/menu-bar.tsx index ce3e7a498e..1817487216 100644 --- a/services/web/frontend/js/features/ide-react/components/toolbar/menu-bar.tsx +++ b/services/web/frontend/js/features/ide-react/components/toolbar/menu-bar.tsx @@ -96,6 +96,7 @@ export const ToolbarMenuBar = () => { 'download-pdf', 'export-as-docx', 'export-as-markdown', + 'export-as-html', ], }, ], diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx index e1272b5439..99be5300d2 100644 --- a/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx +++ b/services/web/frontend/js/features/ide-react/components/toolbar/project-title.tsx @@ -89,6 +89,12 @@ export const ToolbarProjectTitle = () => { label={t('export_as_markdown')} menuBarId="export-as-markdown" /> + RootDocInfo ) { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 7efc57cccc..5b511196cc 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -859,6 +859,7 @@ "explore_all_plans": "Explore all plans", "explore_plans": "Explore plans", "export_as_docx": "Export as Word document (.docx)", + "export_as_html": "Export as HTML (.html)", "export_as_markdown": "Export as Markdown (.md)", "export_csv": "Export CSV", "export_project_to_github": "Export Project to GitHub", @@ -1181,6 +1182,7 @@ "how_to_insert_images": "How to insert images", "how_we_use_your_data": "How we use your data", "how_we_use_your_data_explanation": "<0>Please help us continue to improve Overleaf by answering a few quick questions. Your answers will help us and our corporate group understand more about our user base. We may use this information to improve your Overleaf experience, for example by providing personalized onboarding, upgrade prompts, help suggestions, and tailored marketing communications (if you’ve opted-in to receive them).<1>For more details on how we use your personal data, please see our <0>Privacy Notice.", + "html_export_feedback_message": "Exporting as HTML is a new feature. <0>Let us know what you think", "hundreds_templates_info": "Produce beautiful documents starting from our gallery of LaTeX templates for journals, conferences, theses, reports, CVs and much more.", "i_confirm_am_student": "I confirm that I am currently a student.", "i_want_to_add_a_po_number": "I want to add a PO number", diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs index 2965d5b24a..c65f86186b 100644 --- a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs @@ -541,6 +541,81 @@ describe('ProjectDownloadsController', function () { }) }) + describe('with type=html', function () { + beforeEach(async function (ctx) { + ctx.projectId = '5e9b1c2a3b4c5d6e7f8a9b0c' + ctx.userId = 'test-user-id' + ctx.projectName = 'My Test Project' + ctx.exportStream = { pipe: sinon.stub() } + ctx.contentLength = 9876 + + ctx.req.params = { Project_id: ctx.projectId, type: 'html' } + ctx.req.session = { user: { _id: ctx.userId } } + ctx.req.query = {} + ctx.req.ip = '192.168.1.1' + + ctx.res.attachment = sinon.stub().returns(ctx.res) + + ctx.SessionManager.getLoggedInUserId.returns(ctx.userId) + ctx.ProjectGetter.promises.getProject.resolves({ + name: ctx.projectName, + }) + ctx.DocumentConversionManager.promises.convertProjectToDocument.resolves( + { + conversionId: '12345678-1234-4234-8234-123456789012', + buildId: '0123456789a-0123456789abcdef', + clsiServerId: 'clsi-server-1', + file: 'output.zip', + } + ) + ctx.DocumentConversionManager.promises.streamConvertedProjectDocument.resolves( + { + stream: ctx.exportStream, + contentLength: ctx.contentLength, + } + ) + + await ctx.ProjectDownloadsController.exportProjectConversion( + ctx.req, + ctx.res, + ctx.next + ) + }) + + it('should call convertProjectToDocument with the html type', function (ctx) { + sinon.assert.calledWith( + ctx.DocumentConversionManager.promises.convertProjectToDocument, + ctx.projectId, + ctx.userId, + 'html' + ) + }) + + it('should set the attachment filename with .zip extension', function (ctx) { + sinon.assert.calledWith(ctx.res.attachment, 'My_Test_Project.zip') + }) + + it('should add an audit log entry for html export', function (ctx) { + sinon.assert.calledWith( + ctx.ProjectAuditLogHandler.addEntryInBackground, + ctx.projectId, + 'project-exported-html', + ctx.userId, + ctx.req.ip + ) + }) + + it('should record the action via Metrics with html type', function (ctx) { + ctx.Metrics.inc + .calledWith('document-exports', 1, { type: 'html' }) + .should.equal(true) + }) + + it('should stream the document to the response', function (ctx) { + sinon.assert.calledWith(ctx.pipeline, ctx.exportStream, ctx.res) + }) + }) + describe('when conversion fails with a DocumentConversionError', function () { beforeEach(async function (ctx) { ctx.projectId = '5e9b1c2a3b4c5d6e7f8a9b0c'