Merge pull request #33089 from overleaf/ds-export-md-files-pandoc
[WEB + CLSI] Download as markdown GitOrigin-RevId: 181eddf2513e9c5edacbab37e93f9cac2191ee1a
This commit is contained in:
committed by
Copybot
parent
eddcc5a42e
commit
5dc67db403
@@ -9,7 +9,10 @@ import DocumentConversionManager from '../Uploads/DocumentConversionManager.mjs'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
|
||||
const SUPPORTED_CONVERSION_TYPES = new Map([['docx', 'docx']])
|
||||
const SUPPORTED_CONVERSION_TYPES = new Map([
|
||||
['docx', 'docx'],
|
||||
['markdown', 'zip'],
|
||||
])
|
||||
|
||||
// Keep in sync with the logic for PDF files in CompileController
|
||||
function getSafeProjectName(project) {
|
||||
@@ -30,14 +33,14 @@ async function exportProjectConversion(req, res) {
|
||||
name: true,
|
||||
})
|
||||
|
||||
const safeFileName = getSafeProjectName(project)
|
||||
|
||||
const { stream, contentLength } =
|
||||
await DocumentConversionManager.promises.convertProjectToDocument(
|
||||
projectId,
|
||||
userId,
|
||||
type
|
||||
)
|
||||
|
||||
const safeFileName = getSafeProjectName(project)
|
||||
res.setHeader('Content-Length', contentLength)
|
||||
res.attachment(`${safeFileName}.${extension}`)
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||
|
||||
@@ -483,6 +483,7 @@ const _ProjectController = {
|
||||
'overleaf-code',
|
||||
'export-docx',
|
||||
'sharing-updates',
|
||||
'export-markdown',
|
||||
].filter(Boolean)
|
||||
|
||||
const getUserValues = async userId =>
|
||||
|
||||
@@ -631,6 +631,7 @@
|
||||
"expires_in_days": "",
|
||||
"expires_on": "",
|
||||
"export_as_docx": "",
|
||||
"export_as_markdown": "",
|
||||
"export_csv": "",
|
||||
"export_document_error": "",
|
||||
"export_project_to_github": "",
|
||||
|
||||
@@ -142,3 +142,43 @@ export const ExportProjectDocx = () => {
|
||||
</OLDropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export const ExportProjectMarkdown = () => {
|
||||
const { t } = useTranslation()
|
||||
const { projectId } = useProjectContext()
|
||||
const exportMarkdownEnabled = useFeatureFlag('export-markdown')
|
||||
const enablePandocConversions =
|
||||
getMeta('ol-ExposedSettings')?.enablePandocConversions
|
||||
const anonymous = getMeta('ol-anonymous')
|
||||
|
||||
const showExportMarkdown =
|
||||
exportMarkdownEnabled && enablePandocConversions && !anonymous
|
||||
|
||||
useCommandProvider(
|
||||
() =>
|
||||
showExportMarkdown
|
||||
? [
|
||||
{
|
||||
id: 'export-as-markdown',
|
||||
href: `/project/${projectId}/download/conversion/markdown`,
|
||||
label: t('export_as_markdown'),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[t, showExportMarkdown, projectId]
|
||||
)
|
||||
|
||||
if (!showExportMarkdown) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<OLDropdownMenuItem
|
||||
href={`/project/${projectId}/download/conversion/markdown`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('export_as_markdown')}
|
||||
</OLDropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export const ToolbarMenuBar = () => {
|
||||
'download-as-source-zip',
|
||||
'download-pdf',
|
||||
'export-as-docx',
|
||||
'export-as-markdown',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DownloadProjectPDF,
|
||||
DownloadProjectZip,
|
||||
ExportProjectDocx,
|
||||
ExportProjectMarkdown,
|
||||
} from './download-project'
|
||||
import { useCallback, useState } from 'react'
|
||||
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
|
||||
@@ -81,6 +82,7 @@ export const ToolbarProjectTitle = () => {
|
||||
<DownloadProjectPDF />
|
||||
<DownloadProjectZip />
|
||||
<ExportProjectDocx />
|
||||
<ExportProjectMarkdown />
|
||||
<DropdownDivider />
|
||||
<DuplicateProject />
|
||||
<OLDropdownMenuItem
|
||||
|
||||
@@ -836,6 +836,7 @@
|
||||
"expiry": "Expiry Date",
|
||||
"explore_all_plans": "Explore all plans",
|
||||
"export_as_docx": "Export as Word document (.docx)",
|
||||
"export_as_markdown": "Export as Markdown (.md)",
|
||||
"export_csv": "Export CSV",
|
||||
"export_document_error": "Export failed. Please try again.",
|
||||
"export_project_to_github": "Export Project to GitHub",
|
||||
|
||||
@@ -340,5 +340,71 @@ describe('ProjectDownloadsController', function () {
|
||||
sinon.assert.calledWith(ctx.pipeline, ctx.exportStream, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with type=markdown', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.projectId = 'test-project-id'
|
||||
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: 'markdown' }
|
||||
ctx.req.session = { user: { _id: ctx.userId } }
|
||||
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(
|
||||
{
|
||||
stream: ctx.exportStream,
|
||||
contentLength: ctx.contentLength,
|
||||
}
|
||||
)
|
||||
|
||||
await ctx.ProjectDownloadsController.exportProjectConversion(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
ctx.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should call convertProjectToDocument with the markdown type', function (ctx) {
|
||||
sinon.assert.calledWith(
|
||||
ctx.DocumentConversionManager.promises.convertProjectToDocument,
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
'markdown'
|
||||
)
|
||||
})
|
||||
|
||||
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 markdown export', function (ctx) {
|
||||
sinon.assert.calledWith(
|
||||
ctx.ProjectAuditLogHandler.addEntryInBackground,
|
||||
ctx.projectId,
|
||||
'project-exported-markdown',
|
||||
ctx.userId,
|
||||
ctx.req.ip
|
||||
)
|
||||
})
|
||||
|
||||
it('should record the action via Metrics with markdown type', function (ctx) {
|
||||
ctx.Metrics.inc
|
||||
.calledWith('document-exports', 1, { type: 'markdown' })
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should stream the document to the response', function (ctx) {
|
||||
sinon.assert.calledWith(ctx.pipeline, ctx.exportStream, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -311,7 +311,7 @@ describe('DocumentConversionManager', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
describe('successfully converts the document', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.result =
|
||||
await ctx.DocumentConversionManager.promises.convertProjectToDocument(
|
||||
|
||||
Reference in New Issue
Block a user