mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #33560 from overleaf/mj-conversion-cleanup
[clsi+web] Small cleanups and improvements to conversions / exports GitOrigin-RevId: 300adfbb91e89f754ee7f835db792ccb50b27613
This commit is contained in:
committed by
Copybot
parent
62d92b70dd
commit
6b28a4ee5a
@@ -6,6 +6,79 @@ const MODULE_PATH = Path.join(
|
|||||||
'../../../app/js/ConversionManager'
|
'../../../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 () {
|
describe('ConversionManager', function () {
|
||||||
beforeEach(async function (ctx) {
|
beforeEach(async function (ctx) {
|
||||||
ctx.CommandRunner = {
|
ctx.CommandRunner = {
|
||||||
@@ -65,12 +138,41 @@ describe('ConversionManager', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('convertToLaTeXWithLock', 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) {
|
beforeEach(function (ctx) {
|
||||||
ctx.inputPath = '/path/to/input.docx'
|
ctx.inputPath = '/path/to/input.docx'
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('file setup and pandoc args', function () {
|
describe('successful conversion', function () {
|
||||||
beforeEach(async function (ctx) {
|
beforeEach(async function (ctx) {
|
||||||
ctx.result =
|
ctx.result =
|
||||||
await ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
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)
|
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, {
|
sinon.assert.calledWith(ctx.fs.mkdir, ctx.conversionDir, {
|
||||||
recursive: true,
|
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.firstCall.args[4]).toBe(60_000)
|
||||||
expect(ctx.CommandRunner.promises.run.secondCall.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) {
|
it('should remove the source document after conversion', 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) {
|
|
||||||
sinon.assert.calledWith(
|
sinon.assert.calledWith(
|
||||||
ctx.fs.unlink,
|
ctx.fs.unlink,
|
||||||
Path.join(ctx.conversionDir, 'input.docx')
|
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) {
|
beforeEach(async function (ctx) {
|
||||||
ctx.CommandRunner.promises.run.resolves({
|
ctx.CommandRunner.promises.run.resolves({
|
||||||
stdout: 'mock-stdout',
|
stdout: '',
|
||||||
stderr: 'mock-stderr',
|
stderr: '',
|
||||||
exitCode: 63,
|
exitCode: 63,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
||||||
ctx.conversionId,
|
ctx.conversionId,
|
||||||
@@ -184,7 +236,7 @@ describe('ConversionManager', function () {
|
|||||||
).to.be.rejectedWith('pandoc conversion failed')
|
).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, {
|
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
|
||||||
force: true,
|
force: true,
|
||||||
recursive: 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) {
|
beforeEach(async function (ctx) {
|
||||||
ctx.CommandRunner.promises.run
|
ctx.CommandRunner.promises.run
|
||||||
.onFirstCall()
|
.onFirstCall()
|
||||||
.resolves({
|
.resolves({ stdout: '', stderr: '', exitCode: 0 })
|
||||||
stdout: 'mock-pandoc-stdout',
|
|
||||||
stderr: 'mock-pandoc-stderr',
|
|
||||||
exitCode: 0,
|
|
||||||
})
|
|
||||||
.onSecondCall()
|
.onSecondCall()
|
||||||
.resolves({
|
.resolves({ stdout: '', stderr: '', exitCode: 12 })
|
||||||
stdout: 'mock-zip-stdout',
|
|
||||||
stderr: 'mock-zip-stderr',
|
|
||||||
exitCode: 12,
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
||||||
ctx.conversionId,
|
ctx.conversionId,
|
||||||
@@ -221,7 +264,7 @@ describe('ConversionManager', function () {
|
|||||||
).to.be.rejectedWith('pandoc conversion failed')
|
).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, {
|
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
|
||||||
force: true,
|
force: true,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@@ -247,7 +290,7 @@ describe('ConversionManager', function () {
|
|||||||
).to.be.rejectedWith('pandoc conversion failed')
|
).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, {
|
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
|
||||||
force: true,
|
force: true,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@@ -260,303 +303,153 @@ describe('ConversionManager', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with conversionType=markdown', function () {
|
describe('with an unsupported conversion type', function () {
|
||||||
beforeEach(function (ctx) {
|
it('should reject with an unsupported conversion type error', async function (ctx) {
|
||||||
ctx.inputPath = '/path/to/input.md'
|
await expect(
|
||||||
})
|
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
||||||
|
|
||||||
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([
|
|
||||||
ctx.conversionId,
|
ctx.conversionId,
|
||||||
[
|
'/path/to/input.txt',
|
||||||
'pandoc',
|
'not-a-real-type'
|
||||||
'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')
|
|
||||||
)
|
)
|
||||||
})
|
).to.be.rejectedWith('unsupported conversion type')
|
||||||
|
|
||||||
it('should return the output zip path', function (ctx) {
|
|
||||||
expect(ctx.result).toBe(ctx.outputPath)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('convertLaTeXToDocumentInDirWithLock', function () {
|
describe('convertLaTeXToDocumentInDirWithLock', function () {
|
||||||
describe('successfully', function () {
|
beforeEach(function (ctx) {
|
||||||
beforeEach(async function (ctx) {
|
ctx.compileDir = '/compiles/test-compile-dir'
|
||||||
ctx.compileDir = '/compiles/test-compile-dir'
|
ctx.rootDocPath = 'main.tex'
|
||||||
ctx.rootDocPath = 'main.tex'
|
})
|
||||||
ctx.type = 'docx'
|
|
||||||
ctx.extension = 'docx'
|
|
||||||
|
|
||||||
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(
|
await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
||||||
ctx.conversionId,
|
ctx.conversionId,
|
||||||
ctx.compileDir,
|
ctx.compileDir,
|
||||||
ctx.rootDocPath,
|
ctx.rootDocPath,
|
||||||
ctx.type
|
type
|
||||||
)
|
)
|
||||||
})
|
expect(ctx.CommandRunner.promises.run.firstCall.args[1]).toEqual(
|
||||||
|
pandocArgs('output-uuid')
|
||||||
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}`)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when pandoc fails (non-zero exit code)', function () {
|
describe('with type=docx (representative non-compressing type)', function () {
|
||||||
it('should reject with an error and release the lock', async function (ctx) {
|
describe('successful conversion', function () {
|
||||||
ctx.compileDir = '/compiles/test-compile-dir'
|
beforeEach(async function (ctx) {
|
||||||
|
ctx.result =
|
||||||
ctx.CommandRunner.promises.run.resolves({
|
await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
||||||
stdout: 'mock-stdout',
|
ctx.conversionId,
|
||||||
stderr: 'mock-stderr',
|
ctx.compileDir,
|
||||||
exitCode: 1,
|
ctx.rootDocPath,
|
||||||
|
'docx'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
it('should acquire a lock on the compile dir', function (ctx) {
|
||||||
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
sinon.assert.calledWith(ctx.LockManager.acquire, ctx.compileDir)
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
it('should release the lock', function (ctx) {
|
||||||
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
sinon.assert.called(ctx.lock.release)
|
||||||
ctx.conversionId,
|
})
|
||||||
ctx.compileDir,
|
|
||||||
'main.tex',
|
|
||||||
'markdown'
|
|
||||||
)
|
|
||||||
).to.be.rejectedWith('pandoc latex-to-document conversion failed')
|
|
||||||
|
|
||||||
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 () {
|
describe('with type=markdown (representative compressing type)', function () {
|
||||||
it('should reject with an error and release the lock', async function (ctx) {
|
describe('successful conversion', function () {
|
||||||
ctx.compileDir = '/compiles/test-compile-dir'
|
beforeEach(async function (ctx) {
|
||||||
|
ctx.result =
|
||||||
|
await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
||||||
|
ctx.conversionId,
|
||||||
|
ctx.compileDir,
|
||||||
|
ctx.rootDocPath,
|
||||||
|
'markdown'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
ctx.CommandRunner.promises.run
|
it('should create a UUID-named subdirectory for the output', function (ctx) {
|
||||||
.onFirstCall()
|
sinon.assert.calledWith(
|
||||||
.resolves({ stdout: '', stderr: '', exitCode: 0 })
|
ctx.fs.mkdir,
|
||||||
.onSecondCall()
|
Path.join(ctx.compileDir, 'output-uuid'),
|
||||||
.resolves({ stdout: '', stderr: 'zip error', exitCode: 1 })
|
{ 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(
|
await expect(
|
||||||
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
||||||
ctx.conversionId,
|
ctx.conversionId,
|
||||||
ctx.compileDir,
|
ctx.compileDir,
|
||||||
'main.tex',
|
ctx.rootDocPath,
|
||||||
'markdown'
|
'not-a-real-type'
|
||||||
)
|
)
|
||||||
).to.be.rejectedWith('zip compression of export failed')
|
).to.be.rejectedWith('unsupported conversion type')
|
||||||
|
|
||||||
sinon.assert.called(ctx.lock.release)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -493,6 +493,7 @@
|
|||||||
"document_updated_externally": "",
|
"document_updated_externally": "",
|
||||||
"document_updated_externally_detail": "",
|
"document_updated_externally_detail": "",
|
||||||
"documentation": "",
|
"documentation": "",
|
||||||
|
"docx_export_feedback_message": "",
|
||||||
"docx_import_feedback_message": "",
|
"docx_import_feedback_message": "",
|
||||||
"doesnt_match": "",
|
"doesnt_match": "",
|
||||||
"doing_this_allow_log_in_through_institution": "",
|
"doing_this_allow_log_in_through_institution": "",
|
||||||
@@ -720,6 +721,7 @@
|
|||||||
"generate_latex_from_prompts_and_images": "",
|
"generate_latex_from_prompts_and_images": "",
|
||||||
"generate_token": "",
|
"generate_token": "",
|
||||||
"generating": "",
|
"generating": "",
|
||||||
|
"generic_export_feedback_message": "",
|
||||||
"generic_if_problem_continues_contact_us": "",
|
"generic_if_problem_continues_contact_us": "",
|
||||||
"generic_linked_file_compile_error": "",
|
"generic_linked_file_compile_error": "",
|
||||||
"generic_something_went_wrong": "",
|
"generic_something_went_wrong": "",
|
||||||
@@ -1151,6 +1153,7 @@
|
|||||||
"manager": "",
|
"manager": "",
|
||||||
"managers_management": "",
|
"managers_management": "",
|
||||||
"managing_your_subscription": "",
|
"managing_your_subscription": "",
|
||||||
|
"markdown_export_feedback_message": "",
|
||||||
"markdown_import_feedback_message": "",
|
"markdown_import_feedback_message": "",
|
||||||
"marked_as_resolved": "",
|
"marked_as_resolved": "",
|
||||||
"math": "",
|
"math": "",
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import { useProjectContext } from '@/shared/context/project-context'
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
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 = () => {
|
export const DownloadProjectZip = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -103,75 +100,3 @@ export const DownloadProjectPDF = () => {
|
|||||||
return button
|
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 (
|
|
||||||
<OLDropdownMenuItem onClick={downloadConversion}>
|
|
||||||
{t('export_as_docx')}
|
|
||||||
</OLDropdownMenuItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<OLDropdownMenuItem onClick={downloadConversion}>
|
|
||||||
{t('export_as_markdown')}
|
|
||||||
</OLDropdownMenuItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,6 +24,49 @@ const ExportDocumentErrorToast = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ExportDocumentSuccessToast = ({ data }: { data?: any }) => {
|
||||||
|
const type = data?.type
|
||||||
|
if (type === 'docx') {
|
||||||
|
return (
|
||||||
|
<Trans
|
||||||
|
i18nKey="docx_export_feedback_message"
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||||
|
<a
|
||||||
|
href="https://forms.gle/Fg4BUXV2yv61hStX8"
|
||||||
|
target="_BLANK"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (type === 'markdown') {
|
||||||
|
return (
|
||||||
|
<Trans
|
||||||
|
i18nKey="markdown_export_feedback_message"
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||||
|
<a
|
||||||
|
href="https://forms.gle/wc43zEukeqpec9mAA"
|
||||||
|
target="_BLANK"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Trans
|
||||||
|
i18nKey="generic_export_feedback_message"
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||||
|
<a href="/contact" target="_BLANK" rel="noopener noreferrer" />,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const generators: GlobalToastGeneratorEntry[] = [
|
const generators: GlobalToastGeneratorEntry[] = [
|
||||||
{
|
{
|
||||||
key: 'export-document:error',
|
key: 'export-document:error',
|
||||||
@@ -44,6 +87,16 @@ const generators: GlobalToastGeneratorEntry[] = [
|
|||||||
isDismissible: true,
|
isDismissible: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'export-document:success',
|
||||||
|
generator: (data: any) => ({
|
||||||
|
content: <ExportDocumentSuccessToast data={data} />,
|
||||||
|
type: 'success',
|
||||||
|
autoHide: true,
|
||||||
|
delay: 45000,
|
||||||
|
isDismissible: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default generators
|
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 },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<OLDropdownMenuItem onClick={downloadConversion}>
|
||||||
|
{label}
|
||||||
|
</OLDropdownMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,16 +10,12 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||||
import { useEditorContext } from '@/shared/context/editor-context'
|
import { useEditorContext } from '@/shared/context/editor-context'
|
||||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||||
import {
|
import { DownloadProjectPDF, DownloadProjectZip } from './download-project'
|
||||||
DownloadProjectPDF,
|
|
||||||
DownloadProjectZip,
|
|
||||||
ExportProjectDocx,
|
|
||||||
ExportProjectMarkdown,
|
|
||||||
} from './download-project'
|
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
|
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
|
||||||
import EditableLabel from './editable-label'
|
import EditableLabel from './editable-label'
|
||||||
import { DuplicateProject } from './duplicate-project'
|
import { DuplicateProject } from './duplicate-project'
|
||||||
|
import { ExportProjectWithConversionButton } from './export-project-with-conversion-button'
|
||||||
|
|
||||||
const [publishModalModules] = importOverleafModules('publishModal')
|
const [publishModalModules] = importOverleafModules('publishModal')
|
||||||
const SubmitProjectButton = publishModalModules?.import.NewPublishDropdownButton
|
const SubmitProjectButton = publishModalModules?.import.NewPublishDropdownButton
|
||||||
@@ -81,8 +77,18 @@ export const ToolbarProjectTitle = () => {
|
|||||||
)}
|
)}
|
||||||
<DownloadProjectPDF />
|
<DownloadProjectPDF />
|
||||||
<DownloadProjectZip />
|
<DownloadProjectZip />
|
||||||
<ExportProjectDocx />
|
<ExportProjectWithConversionButton
|
||||||
<ExportProjectMarkdown />
|
featureFlag="export-docx"
|
||||||
|
conversionType="docx"
|
||||||
|
label={t('export_as_docx')}
|
||||||
|
menuBarId="export-as-docx"
|
||||||
|
/>
|
||||||
|
<ExportProjectWithConversionButton
|
||||||
|
featureFlag="export-markdown"
|
||||||
|
conversionType="markdown"
|
||||||
|
label={t('export_as_markdown')}
|
||||||
|
menuBarId="export-as-markdown"
|
||||||
|
/>
|
||||||
<DropdownDivider />
|
<DropdownDivider />
|
||||||
<DuplicateProject />
|
<DuplicateProject />
|
||||||
<OLDropdownMenuItem
|
<OLDropdownMenuItem
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useCallback } from 'react'
|
|||||||
import {
|
import {
|
||||||
hidePreparingExportToast,
|
hidePreparingExportToast,
|
||||||
showExportDocumentError,
|
showExportDocumentError,
|
||||||
|
showExportDocumentSuccess,
|
||||||
showPreparingExportToast,
|
showPreparingExportToast,
|
||||||
} from '../components/toolbar/export-document-toasts'
|
} from '../components/toolbar/export-document-toasts'
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ export default function useConvertProject(type: 'docx' | 'markdown') {
|
|||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
const url = new URL(downloadUrl, window.location.origin)
|
const url = new URL(downloadUrl, window.location.origin)
|
||||||
location.assign(url.toString())
|
location.assign(url.toString())
|
||||||
|
showExportDocumentSuccess(type)
|
||||||
|
} else {
|
||||||
|
showExportDocumentError()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hidePreparingToast()
|
hidePreparingToast()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const MarkdownImportFeedbackToast = () => (
|
|||||||
components={[
|
components={[
|
||||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||||
<a
|
<a
|
||||||
href="https://forms.gle/B1qrdiD983YcQCJA9"
|
href="https://forms.gle/BQnQ57wB9ddS1FdKA"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
/>,
|
/>,
|
||||||
@@ -39,7 +39,8 @@ const generators: GlobalToastGeneratorEntry[] = [
|
|||||||
generator: () => ({
|
generator: () => ({
|
||||||
content: <DocxImportFeedbackToast />,
|
content: <DocxImportFeedbackToast />,
|
||||||
type: 'info',
|
type: 'info',
|
||||||
autoHide: false,
|
autoHide: true,
|
||||||
|
delay: 45000,
|
||||||
isDismissible: true,
|
isDismissible: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -48,7 +49,8 @@ const generators: GlobalToastGeneratorEntry[] = [
|
|||||||
generator: () => ({
|
generator: () => ({
|
||||||
content: <MarkdownImportFeedbackToast />,
|
content: <MarkdownImportFeedbackToast />,
|
||||||
type: 'info',
|
type: 'info',
|
||||||
autoHide: false,
|
autoHide: true,
|
||||||
|
delay: 45000,
|
||||||
isDismissible: true,
|
isDismissible: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.",
|
"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": "Documentation",
|
||||||
"documentation_articles": "Documentation and articles",
|
"documentation_articles": "Documentation and articles",
|
||||||
|
"docx_export_feedback_message": "Exporting Word docs is a new feature. <0>Let us know what you think</0>",
|
||||||
"docx_import_feedback_message": "Importing Word docs is a new feature. <0>Let us know what you think</0>",
|
"docx_import_feedback_message": "Importing Word docs is a new feature. <0>Let us know what you think</0>",
|
||||||
"does_not_contain_or_significantly_match_your_email": "does not contain or significantly match your email",
|
"does_not_contain_or_significantly_match_your_email": "does not contain or significantly match your email",
|
||||||
"doesnt_match": "Doesn’t match",
|
"doesnt_match": "Doesn’t match",
|
||||||
@@ -959,6 +960,7 @@
|
|||||||
"generate_latex_from_prompts_and_images": "Generate LaTeX from prompts and images",
|
"generate_latex_from_prompts_and_images": "Generate LaTeX from prompts and images",
|
||||||
"generate_token": "Generate token",
|
"generate_token": "Generate token",
|
||||||
"generating": "Generating",
|
"generating": "Generating",
|
||||||
|
"generic_export_feedback_message": "This is a new feature. <0>Let us know what you think</0>",
|
||||||
"generic_if_problem_continues_contact_us": "If the problem continues please contact us",
|
"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_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",
|
"generic_something_went_wrong": "Sorry, something went wrong",
|
||||||
@@ -1520,6 +1522,7 @@
|
|||||||
"managers_management": "Managers management",
|
"managers_management": "Managers management",
|
||||||
"managing_your_subscription": "Managing your subscription",
|
"managing_your_subscription": "Managing your subscription",
|
||||||
"march": "March",
|
"march": "March",
|
||||||
|
"markdown_export_feedback_message": "Exporting as Markdown is a new feature. <0>Let us know what you think</0>",
|
||||||
"markdown_import_feedback_message": "Importing Markdown files is a new feature. <0>Let us know what you think</0>",
|
"markdown_import_feedback_message": "Importing Markdown files is a new feature. <0>Let us know what you think</0>",
|
||||||
"marked_as_resolved": "Marked as resolved",
|
"marked_as_resolved": "Marked as resolved",
|
||||||
"math": "Math",
|
"math": "Math",
|
||||||
|
|||||||
Reference in New Issue
Block a user