[WEB + CLSI] Download as docx file feature (#32851)

* using CLSI logic for fetching the project contents and skip the .zip export

* Use unique conversion directory for project-to-docx export to avoid corrupting the shared compile
  directory when a compile runs concurrently

* Remove X-Accel-Buffering header — not needed as CLSI does not run behind nginx

* moving log before sending the data

* Return CLSI stream directly instead of buffering to disk on web

  Previously convertProjectToDocx wrote the CLSI response to a temp file
  on disk, then the controller read it back to stream to the client.
  Now the stream is returned directly and piped to the response,
  avoiding unnecessary disk I/O on the web server.

* Use href redirect for docx export instead of fetching blob into memory

* making functions and files more generic so they can be used in future for other documents exports as well

* adding export-docx split test

* adding unit tests

* adding cypress E2E test

* format:fix

* renaming the route to download from convert

* adding new icon for export docx button

* format:fix

* remove unused showExportDocumentErrorToast export and adding guard against invalid Content-Length header from CLSI

* format:fix

* refactor(clsi): move promisify(parse) into RequestParser

* refactor: generic conversion endpoint with type as route
  param

* refactor: use type→extension map for validated conversion types

* refactor(clsi): remove --standalone flag and fix rejection test

* fixing the href in cypress test

* renaming function

* adding type to Metrics.inc

* fix: rename exportProjectDocument, add WithLock wrapper and metrics type label

* format:fix

* fix: hide docx export from anonymous users and add WithLock wrapper

* format fix

* remove redundant Content-Length validation from DocumentConversionManager

* format:fix

* removing trailing icon

GitOrigin-RevId: e9764fefac2c4b625d23be9e942ea4a8b283c70d
This commit is contained in:
Davinder Singh
2026-04-24 08:06:10 +00:00
committed by Copybot
parent b6ec7945f4
commit be5a7b56c8
20 changed files with 772 additions and 41 deletions
@@ -7,6 +7,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
import { OLToastContainer } from '@/shared/components/ol/ol-toast-container'
import clipboardToastGenerators from '@/features/source-editor/components/clipboard-toasts'
import importDocxFeedbackToastGenerators from '@/features/project-list/components/new-project-button/import-docx-feedback-toast'
import exportDocumentToastGenerators from '@/features/ide-react/components/toolbar/export-document-toasts'
const moduleGeneratorsImport = importOverleafModules('toastGenerators') as {
import: { default: GlobalToastGeneratorEntry[] }
@@ -29,6 +30,7 @@ const GENERATOR_LIST: GlobalToastGeneratorEntry[] = [
...moduleGenerators.flat(),
...clipboardToastGenerators,
...importDocxFeedbackToastGenerators,
...exportDocumentToastGenerators,
]
const GENERATOR_MAP: Map<string, GlobalToastGenerator> = new Map(
GENERATOR_LIST.map(({ key, generator }) => [key, generator])
@@ -7,6 +7,8 @@ import { useProjectContext } from '@/shared/context/project-context'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import getMeta from '@/utils/meta'
import { useFeatureFlag } from '@/shared/context/split-test-context'
export const DownloadProjectZip = () => {
const { t } = useTranslation()
@@ -100,3 +102,43 @@ export const DownloadProjectPDF = () => {
return button
}
}
export const ExportProjectDocx = () => {
const { t } = useTranslation()
const { projectId } = useProjectContext()
const exportDocxEnabled = useFeatureFlag('export-docx')
const enablePandocConversions =
getMeta('ol-ExposedSettings')?.enablePandocConversions
const anonymous = getMeta('ol-anonymous')
const showExportDocx =
exportDocxEnabled && enablePandocConversions && !anonymous
useCommandProvider(
() =>
showExportDocx
? [
{
id: 'export-as-docx',
href: `/project/${projectId}/download/conversion/docx`,
label: t('export_as_docx'),
},
]
: [],
[t, showExportDocx, projectId]
)
if (!showExportDocx) {
return null
}
return (
<OLDropdownMenuItem
href={`/project/${projectId}/download/conversion/docx`}
target="_blank"
rel="noreferrer"
>
{t('export_as_docx')}
</OLDropdownMenuItem>
)
}
@@ -0,0 +1,22 @@
import { GlobalToastGeneratorEntry } from '@/features/ide-react/components/global-toasts'
import { useTranslation } from 'react-i18next'
const ExportDocumentErrorToast = () => {
const { t } = useTranslation()
return <span>{t('export_document_error')}</span>
}
const generators: GlobalToastGeneratorEntry[] = [
{
key: 'export-document:error',
generator: () => ({
content: <ExportDocumentErrorToast />,
type: 'error',
autoHide: true,
delay: 5000,
isDismissible: true,
}),
},
]
export default generators
@@ -87,7 +87,7 @@ export const ToolbarMenuBar = () => {
{ id: 'submit', children: ['submit-project', 'manage-template'] },
{
id: 'file-download',
children: ['download-as-source-zip', 'download-pdf'],
children: ['download-as-source-zip', 'download-pdf', 'export-as-docx'],
},
{
id: 'settings',
@@ -10,7 +10,11 @@ import { useTranslation } from 'react-i18next'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { useEditorContext } from '@/shared/context/editor-context'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { DownloadProjectPDF, DownloadProjectZip } from './download-project'
import {
DownloadProjectPDF,
DownloadProjectZip,
ExportProjectDocx,
} from './download-project'
import { useCallback, useState } from 'react'
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
import EditableLabel from './editable-label'
@@ -76,6 +80,7 @@ export const ToolbarProjectTitle = () => {
)}
<DownloadProjectPDF />
<DownloadProjectZip />
<ExportProjectDocx />
<DropdownDivider />
<DuplicateProject />
<OLDropdownMenuItem