diff --git a/services/clsi/test/unit/js/ConversionManager.test.js b/services/clsi/test/unit/js/ConversionManager.test.js
index c16669124f..bd01c6cae3 100644
--- a/services/clsi/test/unit/js/ConversionManager.test.js
+++ b/services/clsi/test/unit/js/ConversionManager.test.js
@@ -6,6 +6,79 @@ const MODULE_PATH = Path.join(
'../../../app/js/ConversionManager'
)
+const CONVERT_TO_LATEX_CASES = [
+ {
+ type: 'docx',
+ inputFilename: 'input.docx',
+ pandocArgs: [
+ 'pandoc',
+ 'input.docx',
+ '--output',
+ 'main.tex',
+ '--to',
+ 'latex',
+ '--standalone',
+ '--extract-media=.',
+ '--from',
+ 'docx+citations',
+ '--citeproc',
+ ],
+ },
+ {
+ type: 'markdown',
+ inputFilename: 'input.md',
+ pandocArgs: [
+ 'pandoc',
+ 'input.md',
+ '--output',
+ 'main.tex',
+ '--to',
+ 'latex',
+ '--standalone',
+ '--from',
+ 'markdown',
+ ],
+ },
+]
+
+const LATEX_TO_DOCUMENT_CASES = [
+ {
+ type: 'docx',
+ extension: 'docx',
+ compressOutput: false,
+ pandocArgs: outputId => [
+ 'pandoc',
+ 'main.tex',
+ '--output',
+ `${outputId}.docx`,
+ '--from',
+ 'latex',
+ '--to',
+ 'docx',
+ '--citeproc',
+ '--number-sections',
+ '--resource-path=.',
+ ],
+ },
+ {
+ type: 'markdown',
+ extension: 'md',
+ compressOutput: true,
+ pandocArgs: outputId => [
+ 'pandoc',
+ 'main.tex',
+ '--output',
+ Path.join(outputId, 'main.md'),
+ '--from',
+ 'latex',
+ '--to',
+ 'markdown',
+ '--resource-path=.',
+ `--extract-media=${outputId}`,
+ ],
+ },
+]
+
describe('ConversionManager', function () {
beforeEach(async function (ctx) {
ctx.CommandRunner = {
@@ -65,12 +138,41 @@ describe('ConversionManager', function () {
})
describe('convertToLaTeXWithLock', function () {
- describe('with conversionType=docx', function () {
+ describe('per conversion type', function () {
+ CONVERT_TO_LATEX_CASES.forEach(({ type, inputFilename, pandocArgs }) => {
+ describe(`type=${type}`, function () {
+ beforeEach(async function (ctx) {
+ ctx.inputPath = `/path/to/${inputFilename}`
+ await ctx.ConversionManager.promises.convertToLaTeXWithLock(
+ ctx.conversionId,
+ ctx.inputPath,
+ type
+ )
+ })
+
+ it('should copy the input file to the conversion directory under the type-specific filename', function (ctx) {
+ sinon.assert.calledWith(
+ ctx.fs.copyFile,
+ ctx.inputPath,
+ Path.join(ctx.conversionDir, inputFilename)
+ )
+ })
+
+ it('should run pandoc with the type-specific args', function (ctx) {
+ expect(ctx.CommandRunner.promises.run.firstCall.args[1]).toEqual(
+ pandocArgs
+ )
+ })
+ })
+ })
+ })
+
+ describe('with conversionType=docx (representative)', function () {
beforeEach(function (ctx) {
ctx.inputPath = '/path/to/input.docx'
})
- describe('file setup and pandoc args', function () {
+ describe('successful conversion', function () {
beforeEach(async function (ctx) {
ctx.result =
await ctx.ConversionManager.promises.convertToLaTeXWithLock(
@@ -80,78 +182,29 @@ describe('ConversionManager', function () {
)
})
- it('should acquire a lock', async function (ctx) {
+ it('should acquire a lock on the conversion directory', function (ctx) {
sinon.assert.calledWith(ctx.LockManager.acquire, ctx.conversionDir)
})
- it('should copy the input file to the conversion directory with docx filename', async function (ctx) {
+ it('should create the conversion directory', function (ctx) {
sinon.assert.calledWith(ctx.fs.mkdir, ctx.conversionDir, {
recursive: true,
})
- sinon.assert.calledWith(
- ctx.fs.copyFile,
- ctx.inputPath,
- Path.join(ctx.conversionDir, 'input.docx')
- )
})
- it('should convert conversion timeout to milliseconds', async function (ctx) {
+ it('should run pandoc then zip with the conversion timeout in milliseconds', function (ctx) {
+ expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
+ expect(ctx.CommandRunner.promises.run.secondCall.args[1]).toEqual([
+ 'zip',
+ '-r',
+ 'output-uuid.zip',
+ '.',
+ ])
expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
expect(ctx.CommandRunner.promises.run.secondCall.args[4]).toBe(60_000)
})
- it('should run pandoc with docx args followed by zip', function (ctx) {
- expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
- expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
- ctx.conversionId,
- [
- 'pandoc',
- 'input.docx',
- '--output',
- 'main.tex',
- '--to',
- 'latex',
- '--standalone',
- '--extract-media=.',
- '--from',
- 'docx+citations',
- '--citeproc',
- ],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
- ctx.conversionId,
- ['zip', '-r', 'output-uuid.zip', '.'],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
- })
-
- describe('successful conversion', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 0,
- })
-
- ctx.result =
- await ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'docx'
- )
- })
-
- it('should remove the source document after conversion', async function (ctx) {
+ it('should remove the source document after conversion', function (ctx) {
sinon.assert.calledWith(
ctx.fs.unlink,
Path.join(ctx.conversionDir, 'input.docx')
@@ -167,14 +220,13 @@ describe('ConversionManager', function () {
})
})
- describe('unsuccessful conversion (exitcode)', function () {
+ describe('unsuccessful conversion (pandoc exit code)', function () {
beforeEach(async function (ctx) {
ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
+ stdout: '',
+ stderr: '',
exitCode: 63,
})
-
await expect(
ctx.ConversionManager.promises.convertToLaTeXWithLock(
ctx.conversionId,
@@ -184,7 +236,7 @@ describe('ConversionManager', function () {
).to.be.rejectedWith('pandoc conversion failed')
})
- it('should remove the entire conversion directory', async function (ctx) {
+ it('should remove the entire conversion directory', function (ctx) {
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
force: true,
recursive: true,
@@ -196,22 +248,13 @@ describe('ConversionManager', function () {
})
})
- describe('unsuccessful compression (exitcode)', function () {
+ describe('unsuccessful compression (zip exit code)', function () {
beforeEach(async function (ctx) {
ctx.CommandRunner.promises.run
.onFirstCall()
- .resolves({
- stdout: 'mock-pandoc-stdout',
- stderr: 'mock-pandoc-stderr',
- exitCode: 0,
- })
+ .resolves({ stdout: '', stderr: '', exitCode: 0 })
.onSecondCall()
- .resolves({
- stdout: 'mock-zip-stdout',
- stderr: 'mock-zip-stderr',
- exitCode: 12,
- })
-
+ .resolves({ stdout: '', stderr: '', exitCode: 12 })
await expect(
ctx.ConversionManager.promises.convertToLaTeXWithLock(
ctx.conversionId,
@@ -221,7 +264,7 @@ describe('ConversionManager', function () {
).to.be.rejectedWith('pandoc conversion failed')
})
- it('should remove the entire conversion directory', async function (ctx) {
+ it('should remove the entire conversion directory', function (ctx) {
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
force: true,
recursive: true,
@@ -247,7 +290,7 @@ describe('ConversionManager', function () {
).to.be.rejectedWith('pandoc conversion failed')
})
- it('should remove the entire conversion directory', async function (ctx) {
+ it('should remove the entire conversion directory', function (ctx) {
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
force: true,
recursive: true,
@@ -260,303 +303,153 @@ describe('ConversionManager', function () {
})
})
- describe('with conversionType=markdown', function () {
- beforeEach(function (ctx) {
- ctx.inputPath = '/path/to/input.md'
- })
-
- describe('file setup and pandoc args', function () {
- beforeEach(async function (ctx) {
- ctx.result =
- await ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'markdown'
- )
- })
-
- it('should copy the input file to the conversion directory with md filename', async function (ctx) {
- sinon.assert.calledWith(ctx.fs.mkdir, ctx.conversionDir, {
- recursive: true,
- })
- sinon.assert.calledWith(
- ctx.fs.copyFile,
- ctx.inputPath,
- Path.join(ctx.conversionDir, 'input.md')
- )
- })
-
- it('should run pandoc with markdown args followed by zip', function (ctx) {
- expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
- expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
+ describe('with an unsupported conversion type', function () {
+ it('should reject with an unsupported conversion type error', async function (ctx) {
+ await expect(
+ ctx.ConversionManager.promises.convertToLaTeXWithLock(
ctx.conversionId,
- [
- 'pandoc',
- 'input.md',
- '--output',
- 'main.tex',
- '--to',
- 'latex',
- '--standalone',
- '--from',
- 'markdown',
- ],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
- ctx.conversionId,
- ['zip', '-r', 'output-uuid.zip', '.'],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
- })
-
- describe('successful conversion', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 0,
- })
-
- ctx.result =
- await ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'markdown'
- )
- })
-
- it('should remove the source document after conversion', async function (ctx) {
- sinon.assert.calledWith(
- ctx.fs.unlink,
- Path.join(ctx.conversionDir, 'input.md')
+ '/path/to/input.txt',
+ 'not-a-real-type'
)
- })
-
- it('should return the output zip path', function (ctx) {
- expect(ctx.result).toBe(ctx.outputPath)
- })
+ ).to.be.rejectedWith('unsupported conversion type')
})
})
})
describe('convertLaTeXToDocumentInDirWithLock', function () {
- describe('successfully', function () {
- beforeEach(async function (ctx) {
- ctx.compileDir = '/compiles/test-compile-dir'
- ctx.rootDocPath = 'main.tex'
- ctx.type = 'docx'
- ctx.extension = 'docx'
+ beforeEach(function (ctx) {
+ ctx.compileDir = '/compiles/test-compile-dir'
+ ctx.rootDocPath = 'main.tex'
+ })
- ctx.result =
+ describe('pandoc args per conversion type', function () {
+ LATEX_TO_DOCUMENT_CASES.forEach(({ type, pandocArgs }) => {
+ it(`should run pandoc with the type-specific args for type=${type}`, async function (ctx) {
await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
ctx.conversionId,
ctx.compileDir,
ctx.rootDocPath,
- ctx.type
+ type
)
- })
-
- it('should acquire a lock on the compile dir', function (ctx) {
- sinon.assert.calledWith(ctx.LockManager.acquire, ctx.compileDir)
- })
-
- it('should release the lock', function (ctx) {
- sinon.assert.called(ctx.lock.release)
- })
-
- it('should run pandoc with correct arguments', function (ctx) {
- expect(ctx.CommandRunner.promises.run.callCount).toBe(1)
- expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
- ctx.conversionId,
- [
- 'pandoc',
- ctx.rootDocPath,
- '--output',
- `output-uuid.${ctx.extension}`,
- '--from',
- 'latex',
- '--to',
- ctx.type,
- '--citeproc',
- '--number-sections',
- '--resource-path=.',
- ],
- ctx.compileDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
-
- it('should convert conversion timeout to milliseconds', function (ctx) {
- expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
- })
-
- it('should return path to the output document', function (ctx) {
- expect(ctx.result).toBe(
- Path.join(ctx.compileDir, `output-uuid.${ctx.extension}`)
- )
+ expect(ctx.CommandRunner.promises.run.firstCall.args[1]).toEqual(
+ pandocArgs('output-uuid')
+ )
+ })
})
})
- describe('when pandoc fails (non-zero exit code)', function () {
- it('should reject with an error and release the lock', async function (ctx) {
- ctx.compileDir = '/compiles/test-compile-dir'
-
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 1,
+ describe('with type=docx (representative non-compressing type)', function () {
+ describe('successful conversion', function () {
+ beforeEach(async function (ctx) {
+ ctx.result =
+ await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
+ ctx.conversionId,
+ ctx.compileDir,
+ ctx.rootDocPath,
+ 'docx'
+ )
})
- await expect(
- ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
- ctx.conversionId,
- ctx.compileDir,
- 'main.tex',
- 'docx'
- )
- ).to.be.rejectedWith('pandoc latex-to-document conversion failed')
-
- sinon.assert.called(ctx.lock.release)
- })
- })
- })
-
- describe('convertLaTeXToDocumentInDirWithLock (type=markdown)', function () {
- describe('successfully', function () {
- beforeEach(async function (ctx) {
- ctx.compileDir = '/compiles/test-compile-dir'
- ctx.rootDocPath = 'main.tex'
-
- ctx.result =
- await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
- ctx.conversionId,
- ctx.compileDir,
- ctx.rootDocPath,
- 'markdown'
- )
- })
-
- it('should acquire a lock on the compile dir', function (ctx) {
- sinon.assert.calledWith(ctx.LockManager.acquire, ctx.compileDir)
- })
-
- it('should release the lock', function (ctx) {
- sinon.assert.called(ctx.lock.release)
- })
-
- it('should create a UUID-named subdirectory', function (ctx) {
- sinon.assert.calledWith(
- ctx.fs.mkdir,
- Path.join(ctx.compileDir, 'output-uuid'),
- { recursive: true }
- )
- })
-
- it('should run pandoc then zip (two commands total)', function (ctx) {
- expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
- })
-
- it('should run pandoc outputting main.md into the UUID-named subdir', function (ctx) {
- expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
- ctx.conversionId,
- [
- 'pandoc',
- ctx.rootDocPath,
- '--output',
- Path.join('output-uuid', 'main.md'),
- '--from',
- 'latex',
- '--to',
- 'markdown',
- '--resource-path=.',
- '--extract-media=output-uuid',
- ],
- ctx.compileDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
-
- it('should zip the project-named subdirectory', function (ctx) {
- expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
- ctx.conversionId,
- ['sh', '-c', 'cd output-uuid && zip -r ../output-uuid.zip .'],
- ctx.compileDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
-
- it('should return the path to the zip file', function (ctx) {
- expect(ctx.result).toBe(Path.join(ctx.compileDir, 'output-uuid.zip'))
- })
-
- it('should convert conversion timeout to milliseconds', function (ctx) {
- expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
- expect(ctx.CommandRunner.promises.run.secondCall.args[4]).toBe(60_000)
- })
- })
-
- describe('when pandoc fails (non-zero exit code)', function () {
- it('should reject with an error and release the lock', async function (ctx) {
- ctx.compileDir = '/compiles/test-compile-dir'
-
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 1,
+ it('should acquire a lock on the compile dir', function (ctx) {
+ sinon.assert.calledWith(ctx.LockManager.acquire, ctx.compileDir)
})
- await expect(
- ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
- ctx.conversionId,
- ctx.compileDir,
- 'main.tex',
- 'markdown'
- )
- ).to.be.rejectedWith('pandoc latex-to-document conversion failed')
+ it('should release the lock', function (ctx) {
+ sinon.assert.called(ctx.lock.release)
+ })
- sinon.assert.called(ctx.lock.release)
+ it('should pass the conversion timeout in milliseconds', function (ctx) {
+ expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
+ })
+
+ it('should not create a subdirectory or run zip and should return the document path directly', function (ctx) {
+ sinon.assert.notCalled(ctx.fs.mkdir)
+ expect(ctx.CommandRunner.promises.run.callCount).toBe(1)
+ expect(ctx.result).toBe(Path.join(ctx.compileDir, 'output-uuid.docx'))
+ })
+ })
+
+ describe('when pandoc fails (non-zero exit code)', function () {
+ it('should reject with an error and release the lock', async function (ctx) {
+ ctx.CommandRunner.promises.run.resolves({
+ stdout: '',
+ stderr: '',
+ exitCode: 1,
+ })
+ await expect(
+ ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
+ ctx.conversionId,
+ ctx.compileDir,
+ ctx.rootDocPath,
+ 'docx'
+ )
+ ).to.be.rejectedWith('pandoc latex-to-document conversion failed')
+ sinon.assert.called(ctx.lock.release)
+ })
})
})
- describe('when zip fails (non-zero exit code)', function () {
- it('should reject with an error and release the lock', async function (ctx) {
- ctx.compileDir = '/compiles/test-compile-dir'
+ describe('with type=markdown (representative compressing type)', function () {
+ describe('successful conversion', function () {
+ beforeEach(async function (ctx) {
+ ctx.result =
+ await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
+ ctx.conversionId,
+ ctx.compileDir,
+ ctx.rootDocPath,
+ 'markdown'
+ )
+ })
- ctx.CommandRunner.promises.run
- .onFirstCall()
- .resolves({ stdout: '', stderr: '', exitCode: 0 })
- .onSecondCall()
- .resolves({ stdout: '', stderr: 'zip error', exitCode: 1 })
+ it('should create a UUID-named subdirectory for the output', function (ctx) {
+ sinon.assert.calledWith(
+ ctx.fs.mkdir,
+ Path.join(ctx.compileDir, 'output-uuid'),
+ { recursive: true }
+ )
+ })
+ it('should run pandoc then zip the subdirectory and return the zip path', function (ctx) {
+ expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
+ expect(ctx.CommandRunner.promises.run.secondCall.args[1]).toEqual([
+ 'sh',
+ '-c',
+ 'cd output-uuid && zip -r ../output-uuid.zip .',
+ ])
+ expect(ctx.result).toBe(Path.join(ctx.compileDir, 'output-uuid.zip'))
+ })
+ })
+
+ describe('when zip fails (non-zero exit code)', function () {
+ it('should reject with an error and release the lock', async function (ctx) {
+ ctx.CommandRunner.promises.run
+ .onFirstCall()
+ .resolves({ stdout: '', stderr: '', exitCode: 0 })
+ .onSecondCall()
+ .resolves({ stdout: '', stderr: 'zip error', exitCode: 1 })
+ await expect(
+ ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
+ ctx.conversionId,
+ ctx.compileDir,
+ ctx.rootDocPath,
+ 'markdown'
+ )
+ ).to.be.rejectedWith('zip compression of export failed')
+ sinon.assert.called(ctx.lock.release)
+ })
+ })
+ })
+
+ describe('with an unsupported conversion type', function () {
+ it('should reject with an unsupported conversion type error', async function (ctx) {
await expect(
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
ctx.conversionId,
ctx.compileDir,
- 'main.tex',
- 'markdown'
+ ctx.rootDocPath,
+ 'not-a-real-type'
)
- ).to.be.rejectedWith('zip compression of export failed')
-
- sinon.assert.called(ctx.lock.release)
+ ).to.be.rejectedWith('unsupported conversion type')
})
})
})
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 3b801d4baf..75aed5fc31 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -493,6 +493,7 @@
"document_updated_externally": "",
"document_updated_externally_detail": "",
"documentation": "",
+ "docx_export_feedback_message": "",
"docx_import_feedback_message": "",
"doesnt_match": "",
"doing_this_allow_log_in_through_institution": "",
@@ -720,6 +721,7 @@
"generate_latex_from_prompts_and_images": "",
"generate_token": "",
"generating": "",
+ "generic_export_feedback_message": "",
"generic_if_problem_continues_contact_us": "",
"generic_linked_file_compile_error": "",
"generic_something_went_wrong": "",
@@ -1151,6 +1153,7 @@
"manager": "",
"managers_management": "",
"managing_your_subscription": "",
+ "markdown_export_feedback_message": "",
"markdown_import_feedback_message": "",
"marked_as_resolved": "",
"math": "",
diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/download-project.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/download-project.tsx
index 139eb78ff3..16cb17774e 100644
--- a/services/web/frontend/js/features/ide-react/components/toolbar/download-project.tsx
+++ b/services/web/frontend/js/features/ide-react/components/toolbar/download-project.tsx
@@ -7,9 +7,6 @@ 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'
-import useConvertProject from '../../hooks/use-convert-project'
export const DownloadProjectZip = () => {
const { t } = useTranslation()
@@ -103,75 +100,3 @@ export const DownloadProjectPDF = () => {
return button
}
}
-
-export const ExportProjectDocx = () => {
- const { t } = useTranslation()
- const exportDocxEnabled = useFeatureFlag('export-docx')
- const enablePandocConversions =
- getMeta('ol-ExposedSettings')?.enablePandocConversions
- const anonymous = getMeta('ol-anonymous')
- const downloadConversion = useConvertProject('docx')
-
- const showExportDocx =
- exportDocxEnabled && enablePandocConversions && !anonymous
-
- useCommandProvider(
- () =>
- showExportDocx
- ? [
- {
- id: 'export-as-docx',
- handler: downloadConversion,
- label: t('export_as_docx'),
- },
- ]
- : [],
- [t, showExportDocx, downloadConversion]
- )
-
- if (!showExportDocx) {
- return null
- }
-
- return (
-
- {t('export_as_docx')}
-
- )
-}
-
-export const ExportProjectMarkdown = () => {
- const { t } = useTranslation()
- const exportMarkdownEnabled = useFeatureFlag('export-markdown')
- const enablePandocConversions =
- getMeta('ol-ExposedSettings')?.enablePandocConversions
- const anonymous = getMeta('ol-anonymous')
- const downloadConversion = useConvertProject('markdown')
-
- const showExportMarkdown =
- exportMarkdownEnabled && enablePandocConversions && !anonymous
-
- useCommandProvider(
- () =>
- showExportMarkdown
- ? [
- {
- id: 'export-as-markdown',
- handler: downloadConversion,
- label: t('export_as_markdown'),
- },
- ]
- : [],
- [t, showExportMarkdown, downloadConversion]
- )
-
- if (!showExportMarkdown) {
- return null
- }
-
- return (
-
- {t('export_as_markdown')}
-
- )
-}
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 34ab19187d..bf9c57b33e 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
@@ -24,6 +24,49 @@ const ExportDocumentErrorToast = () => {
)
}
+const ExportDocumentSuccessToast = ({ data }: { data?: any }) => {
+ const type = data?.type
+ if (type === 'docx') {
+ return (
+ ,
+ ]}
+ />
+ )
+ } else if (type === 'markdown') {
+ return (
+ ,
+ ]}
+ />
+ )
+ } else {
+ return (
+ ,
+ ]}
+ />
+ )
+ }
+}
+
const generators: GlobalToastGeneratorEntry[] = [
{
key: 'export-document:error',
@@ -44,6 +87,16 @@ const generators: GlobalToastGeneratorEntry[] = [
isDismissible: true,
}),
},
+ {
+ key: 'export-document:success',
+ generator: (data: any) => ({
+ content: ,
+ type: 'success',
+ autoHide: true,
+ delay: 45000,
+ isDismissible: true,
+ }),
+ },
]
export default generators
@@ -73,3 +126,11 @@ export const hidePreparingExportToast = (handle: string) => {
})
)
}
+
+export const showExportDocumentSuccess = (type: 'docx' | 'markdown') => {
+ 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
new file mode 100644
index 0000000000..9cc84e405f
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/components/toolbar/export-project-with-conversion-button.tsx
@@ -0,0 +1,51 @@
+import getMeta from '@/utils/meta'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import { FC } from 'react'
+import useConvertProject from '../../hooks/use-convert-project'
+import { useCommandProvider } from '../../hooks/use-command-provider'
+import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
+
+type ExportProjectWithConversionProps = {
+ featureFlag?: string
+ conversionType: 'docx' | 'markdown'
+ label: string
+ menuBarId: string
+}
+export const ExportProjectWithConversionButton: FC<
+ ExportProjectWithConversionProps
+> = ({ featureFlag, conversionType, label, menuBarId }) => {
+ const splitTestEnabledIfNeeded = featureFlag
+ ? isSplitTestEnabled(featureFlag)
+ : true
+ const enablePandocConversions =
+ getMeta('ol-ExposedSettings')?.enablePandocConversions
+ const anonymous = getMeta('ol-anonymous')
+ const downloadConversion = useConvertProject(conversionType)
+
+ const showExportButton =
+ splitTestEnabledIfNeeded && enablePandocConversions && !anonymous
+
+ useCommandProvider(
+ () =>
+ showExportButton
+ ? [
+ {
+ id: menuBarId,
+ handler: downloadConversion,
+ label,
+ },
+ ]
+ : [],
+ [showExportButton, downloadConversion, label, menuBarId]
+ )
+
+ if (!showExportButton) {
+ return null
+ }
+
+ return (
+
+ {label}
+
+ )
+}
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 45bd5974c0..e1272b5439 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
@@ -10,16 +10,12 @@ 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,
- ExportProjectDocx,
- ExportProjectMarkdown,
-} from './download-project'
+import { DownloadProjectPDF, DownloadProjectZip } from './download-project'
import { useCallback, useState } from 'react'
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
import EditableLabel from './editable-label'
import { DuplicateProject } from './duplicate-project'
+import { ExportProjectWithConversionButton } from './export-project-with-conversion-button'
const [publishModalModules] = importOverleafModules('publishModal')
const SubmitProjectButton = publishModalModules?.import.NewPublishDropdownButton
@@ -81,8 +77,18 @@ export const ToolbarProjectTitle = () => {
)}
-
-
+
+
(
components={[
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
,
@@ -39,7 +39,8 @@ const generators: GlobalToastGeneratorEntry[] = [
generator: () => ({
content: ,
type: 'info',
- autoHide: false,
+ autoHide: true,
+ delay: 45000,
isDismissible: true,
}),
},
@@ -48,7 +49,8 @@ const generators: GlobalToastGeneratorEntry[] = [
generator: () => ({
content: ,
type: 'info',
- autoHide: false,
+ autoHide: true,
+ delay: 45000,
isDismissible: true,
}),
},
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 49610a3194..4dda8d4e5d 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -653,6 +653,7 @@
"document_updated_externally_detail": "This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions, please look in the history.",
"documentation": "Documentation",
"documentation_articles": "Documentation and articles",
+ "docx_export_feedback_message": "Exporting Word docs is a new feature. <0>Let us know what you think0>",
"docx_import_feedback_message": "Importing Word docs is a new feature. <0>Let us know what you think0>",
"does_not_contain_or_significantly_match_your_email": "does not contain or significantly match your email",
"doesnt_match": "Doesn’t match",
@@ -959,6 +960,7 @@
"generate_latex_from_prompts_and_images": "Generate LaTeX from prompts and images",
"generate_token": "Generate token",
"generating": "Generating",
+ "generic_export_feedback_message": "This is a new feature. <0>Let us know what you think0>",
"generic_if_problem_continues_contact_us": "If the problem continues please contact us",
"generic_linked_file_compile_error": "This project’s output files are not available because it failed to compile. Please open the project to see the compilation error details.",
"generic_something_went_wrong": "Sorry, something went wrong",
@@ -1520,6 +1522,7 @@
"managers_management": "Managers management",
"managing_your_subscription": "Managing your subscription",
"march": "March",
+ "markdown_export_feedback_message": "Exporting as Markdown is a new feature. <0>Let us know what you think0>",
"markdown_import_feedback_message": "Importing Markdown files is a new feature. <0>Let us know what you think0>",
"marked_as_resolved": "Marked as resolved",
"math": "Math",