✨(frontend) enable ODT export for documents
provides ODT export with support for callout, upload, interlinking and tests Signed-off-by: Cyril <c.gromoff@gmail.com> ✨(frontend) add image and interlinking support for odt export Added image mapping with SVG conversion and clickable document links. Signed-off-by: Cyril <c.gromoff@gmail.com> ✅(e2e) add e2e tests for odt export and interlinking features covers odt document export and cross-section interlinking use cases Signed-off-by: Cyril <c.gromoff@gmail.com> ✨(odt) add generic helper and style callout block for odt export create odtRegisterParagraphStyleForBlock and apply background/padding styles Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
@@ -6,6 +6,10 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) enable ODT export for documents #1524
|
||||
|
||||
### Fixed
|
||||
|
||||
- ♿(frontend) improve accessibility:
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('Doc Export', () => {
|
||||
|
||||
await expect(page.getByTestId('modal-export-title')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('Download your document in a .docx or .pdf format.'),
|
||||
page.getByText('Download your document in a .docx, .odt or .pdf format.'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('combobox', { name: 'Template' }),
|
||||
@@ -142,6 +142,51 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
|
||||
});
|
||||
|
||||
test('it exports the doc to odt', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor-odt', browserName, 1);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World ODT');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByText('Upload image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
|
||||
|
||||
const image = page
|
||||
.locator('.--docs--editor-container img.bn-visual-media')
|
||||
.first();
|
||||
|
||||
await expect(image).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Odt' }).click();
|
||||
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.odt`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.odt`);
|
||||
});
|
||||
|
||||
/**
|
||||
* This test tell us that the export to pdf is working with images
|
||||
* but it does not tell us if the images are being displayed correctly
|
||||
@@ -442,4 +487,68 @@ test.describe('Doc Export', () => {
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain(randomDoc);
|
||||
});
|
||||
|
||||
test('it exports the doc with interlinking to odt', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'export-interlinking-odt',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'export-interlink-child-odt',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
const input = page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
);
|
||||
const searchContainer = page.locator('.quick-search-container');
|
||||
|
||||
await input.fill('export-interlink');
|
||||
|
||||
await expect(searchContainer).toBeVisible();
|
||||
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
|
||||
|
||||
// We are in docChild, we want to create a link to randomDoc (parent)
|
||||
await searchContainer.getByText(randomDoc).click();
|
||||
|
||||
// Search the interlinking link in the editor (not in the document tree)
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
const interlink = editor.getByRole('button', {
|
||||
name: randomDoc,
|
||||
});
|
||||
|
||||
await expect(interlink).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Odt' }).click();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${docChild}.odt`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@blocknote/react": "0.41.1",
|
||||
"@blocknote/xl-docx-exporter": "0.41.1",
|
||||
"@blocknote/xl-multi-column": "0.41.1",
|
||||
"@blocknote/xl-odt-exporter": "0.41.1",
|
||||
"@blocknote/xl-pdf-exporter": "0.41.1",
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
"@dnd-kit/modifiers": "9.0.0",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DocsExporterODT } from '../types';
|
||||
import { odtRegisterParagraphStyleForBlock } from '../utils';
|
||||
|
||||
export const blockMappingCalloutODT: DocsExporterODT['mappings']['blockMapping']['callout'] =
|
||||
(block, exporter) => {
|
||||
// Map callout to paragraph with emoji prefix
|
||||
const emoji = block.props.emoji || '💡';
|
||||
|
||||
// Transform inline content (text, bold, links, etc.)
|
||||
const inlineContent = exporter.transformInlineContent(block.content);
|
||||
|
||||
// Resolve background and alignment → create a dedicated paragraph style
|
||||
const styleName = odtRegisterParagraphStyleForBlock(
|
||||
exporter,
|
||||
{
|
||||
backgroundColor: block.props.backgroundColor,
|
||||
textAlignment: block.props.textAlignment,
|
||||
},
|
||||
{ paddingCm: 0.42 },
|
||||
);
|
||||
|
||||
return React.createElement(
|
||||
'text:p',
|
||||
{
|
||||
'text:style-name': styleName,
|
||||
},
|
||||
`${emoji} `,
|
||||
...inlineContent,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DocsExporterODT } from '../types';
|
||||
import { convertSvgToPng, odtRegisterParagraphStyleForBlock } from '../utils';
|
||||
|
||||
const MAX_WIDTH = 600;
|
||||
|
||||
export const blockMappingImageODT: DocsExporterODT['mappings']['blockMapping']['image'] =
|
||||
async (block, exporter) => {
|
||||
try {
|
||||
const blob = await exporter.resolveFile(block.props.url);
|
||||
|
||||
if (!blob || !blob.type) {
|
||||
console.warn(`Failed to resolve image: ${block.props.url}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let pngConverted: string | undefined;
|
||||
let dimensions: { width: number; height: number } | undefined;
|
||||
let previewWidth = block.props.previewWidth || undefined;
|
||||
|
||||
if (!blob.type.includes('image')) {
|
||||
console.warn(`Not an image type: ${blob.type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blob.type.includes('svg')) {
|
||||
const svgText = await blob.text();
|
||||
const FALLBACK_SIZE = 536;
|
||||
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
|
||||
pngConverted = await convertSvgToPng(svgText, previewWidth);
|
||||
const img = new Image();
|
||||
img.src = pngConverted;
|
||||
await new Promise((resolve) => {
|
||||
img.onload = () => {
|
||||
dimensions = { width: img.width, height: img.height };
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
} else {
|
||||
dimensions = await getImageDimensions(blob);
|
||||
}
|
||||
|
||||
if (!dimensions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { width, height } = dimensions;
|
||||
|
||||
if (previewWidth && previewWidth > MAX_WIDTH) {
|
||||
previewWidth = MAX_WIDTH;
|
||||
}
|
||||
|
||||
// Convert image to base64 for ODT embedding
|
||||
const arrayBuffer = pngConverted
|
||||
? await (await fetch(pngConverted)).arrayBuffer()
|
||||
: await blob.arrayBuffer();
|
||||
const base64 = btoa(
|
||||
Array.from(new Uint8Array(arrayBuffer))
|
||||
.map((byte) => String.fromCharCode(byte))
|
||||
.join(''),
|
||||
);
|
||||
|
||||
const finalWidth = previewWidth || width;
|
||||
const finalHeight = ((previewWidth || width) / width) * height;
|
||||
|
||||
const baseParagraphProps = {
|
||||
backgroundColor: block.props.backgroundColor,
|
||||
textAlignment: block.props.textAlignment,
|
||||
};
|
||||
|
||||
const paragraphStyleName = odtRegisterParagraphStyleForBlock(
|
||||
exporter,
|
||||
baseParagraphProps,
|
||||
{ paddingCm: 0 },
|
||||
);
|
||||
|
||||
// Convert pixels to cm (ODT uses cm for dimensions)
|
||||
const widthCm = finalWidth / 37.795275591;
|
||||
const heightCm = finalHeight / 37.795275591;
|
||||
|
||||
// Create ODT image structure using React.createElement
|
||||
const frame = React.createElement(
|
||||
'text:p',
|
||||
{
|
||||
'text:style-name': paragraphStyleName,
|
||||
},
|
||||
React.createElement(
|
||||
'draw:frame',
|
||||
{
|
||||
'draw:name': `Image${Date.now()}`,
|
||||
'text:anchor-type': 'as-char',
|
||||
'svg:width': `${widthCm}cm`,
|
||||
'svg:height': `${heightCm}cm`,
|
||||
},
|
||||
React.createElement(
|
||||
'draw:image',
|
||||
{
|
||||
xlinkType: 'simple',
|
||||
xlinkShow: 'embed',
|
||||
xlinkActuate: 'onLoad',
|
||||
},
|
||||
React.createElement('office:binary-data', {}, base64),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Add caption if present
|
||||
if (block.props.caption) {
|
||||
const captionStyleName = odtRegisterParagraphStyleForBlock(
|
||||
exporter,
|
||||
baseParagraphProps,
|
||||
{ paddingCm: 0, parentStyleName: 'Caption' },
|
||||
);
|
||||
|
||||
return [
|
||||
frame,
|
||||
React.createElement(
|
||||
'text:p',
|
||||
{ 'text:style-name': captionStyleName },
|
||||
block.props.caption,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return frame;
|
||||
} catch (error) {
|
||||
console.error(`Error processing image for ODT export:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async function getImageDimensions(blob: Blob) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const bmp = await createImageBitmap(blob);
|
||||
const { width, height } = bmp;
|
||||
bmp.close();
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
export * from './calloutDocx';
|
||||
export * from './calloutODT';
|
||||
export * from './calloutPDF';
|
||||
export * from './headingPDF';
|
||||
export * from './imageDocx';
|
||||
export * from './imageODT';
|
||||
export * from './imagePDF';
|
||||
export * from './paragraphPDF';
|
||||
export * from './quoteDocx';
|
||||
export * from './quotePDF';
|
||||
export * from './tablePDF';
|
||||
export * from './uploadLoaderPDF';
|
||||
export * from './uploadLoaderDocx';
|
||||
export * from './uploadLoaderODT';
|
||||
export * from './uploadLoaderPDF';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DocsExporterODT } from '../types';
|
||||
|
||||
export const blockMappingUploadLoaderODT: DocsExporterODT['mappings']['blockMapping']['uploadLoader'] =
|
||||
(block) => {
|
||||
// Map uploadLoader to paragraph with information text
|
||||
const information = block.props.information || '';
|
||||
const type = block.props.type || 'loading';
|
||||
const prefix = type === 'warning' ? '⚠️ ' : '⏳ ';
|
||||
|
||||
return React.createElement(
|
||||
'text:p',
|
||||
{ 'text:style-name': 'Text_20_body' },
|
||||
`${prefix}${information}`,
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DOCXExporter } from '@blocknote/xl-docx-exporter';
|
||||
import { ODTExporter } from '@blocknote/xl-odt-exporter';
|
||||
import { PDFExporter } from '@blocknote/xl-pdf-exporter';
|
||||
import {
|
||||
Button,
|
||||
@@ -23,12 +24,14 @@ import { Doc, useTrans } from '@/docs/doc-management';
|
||||
import { exportCorsResolveFileUrl } from '../api/exportResolveFileUrl';
|
||||
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
||||
import { docxDocsSchemaMappings } from '../mappingDocx';
|
||||
import { odtDocsSchemaMappings } from '../mappingODT';
|
||||
import { pdfDocsSchemaMappings } from '../mappingPDF';
|
||||
import { downloadFile } from '../utils';
|
||||
|
||||
enum DocDownloadFormat {
|
||||
PDF = 'pdf',
|
||||
DOCX = 'docx',
|
||||
ODT = 'odt',
|
||||
}
|
||||
|
||||
interface ModalExportProps {
|
||||
@@ -124,7 +127,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
: rawPdfDocument;
|
||||
|
||||
blobExport = await pdf(pdfDocument).toBlob();
|
||||
} else {
|
||||
} else if (format === DocDownloadFormat.DOCX) {
|
||||
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {
|
||||
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
|
||||
});
|
||||
@@ -133,6 +136,16 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
documentOptions: { title: documentTitle },
|
||||
sectionOptions: {},
|
||||
});
|
||||
} else if (format === DocDownloadFormat.ODT) {
|
||||
const exporter = new ODTExporter(editor.schema, odtDocsSchemaMappings, {
|
||||
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
|
||||
});
|
||||
|
||||
blobExport = await exporter.toODTDocument(exportDocument);
|
||||
} else {
|
||||
toast(t('The export failed'), VariantType.ERROR);
|
||||
setIsExporting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadFile(blobExport, `${filename}.${format}`);
|
||||
@@ -213,7 +226,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
className="--docs--modal-export-content"
|
||||
>
|
||||
<Text $variation="600" $size="sm" as="p">
|
||||
{t('Download your document in a .docx or .pdf format.')}
|
||||
{t('Download your document in a .docx, .odt or .pdf format.')}
|
||||
</Text>
|
||||
<Select
|
||||
clearable={false}
|
||||
@@ -231,6 +244,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
label={t('Format')}
|
||||
options={[
|
||||
{ label: t('Docx'), value: DocDownloadFormat.DOCX },
|
||||
{ label: t('ODT'), value: DocDownloadFormat.ODT },
|
||||
{ label: t('PDF'), value: DocDownloadFormat.PDF },
|
||||
]}
|
||||
value={format}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './interlinkingLinkPDF';
|
||||
export * from './interlinkingLinkDocx';
|
||||
export * from './interlinkingLinkODT';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DocsExporterODT } from '../types';
|
||||
|
||||
export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
|
||||
(inline) => {
|
||||
const url = window.location.origin + inline.props.url;
|
||||
const title = inline.props.title;
|
||||
|
||||
// Create ODT hyperlink using React.createElement to avoid TypeScript JSX namespace issues
|
||||
// Uses the same structure as BlockNote's default link mapping
|
||||
return React.createElement(
|
||||
'text:a',
|
||||
{
|
||||
xlinkType: 'simple',
|
||||
'text:style-name': 'Internet_20_link',
|
||||
'office:target-frame-name': '_top',
|
||||
xlinkShow: 'replace',
|
||||
xlinkHref: url,
|
||||
},
|
||||
`📄${title}`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { odtDefaultSchemaMappings } from '@blocknote/xl-odt-exporter';
|
||||
|
||||
import {
|
||||
blockMappingCalloutODT,
|
||||
blockMappingImageODT,
|
||||
blockMappingUploadLoaderODT,
|
||||
} from './blocks-mapping';
|
||||
import { inlineContentMappingInterlinkingLinkODT } from './inline-content-mapping';
|
||||
import { DocsExporterODT } from './types';
|
||||
|
||||
// Align default inline mappings to our editor inline schema without using `any`
|
||||
const baseInlineMappings =
|
||||
odtDefaultSchemaMappings.inlineContentMapping as unknown as DocsExporterODT['mappings']['inlineContentMapping'];
|
||||
|
||||
export const odtDocsSchemaMappings: DocsExporterODT['mappings'] = {
|
||||
...odtDefaultSchemaMappings,
|
||||
blockMapping: {
|
||||
...odtDefaultSchemaMappings.blockMapping,
|
||||
callout: blockMappingCalloutODT,
|
||||
image: blockMappingImageODT,
|
||||
// We're reusing the file block mapping for PDF blocks
|
||||
// The types don't match exactly but the implementation is compatible
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pdf: odtDefaultSchemaMappings.blockMapping.file as any,
|
||||
uploadLoader: blockMappingUploadLoaderODT,
|
||||
},
|
||||
|
||||
inlineContentMapping: {
|
||||
...baseInlineMappings,
|
||||
interlinkingSearchInline: () => null,
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkODT,
|
||||
},
|
||||
styleMapping: {
|
||||
...odtDefaultSchemaMappings.styleMapping,
|
||||
},
|
||||
};
|
||||
@@ -51,3 +51,13 @@ export type DocsExporterDocx = Exporter<
|
||||
IRunPropertiesOptions,
|
||||
TextRun
|
||||
>;
|
||||
|
||||
export type DocsExporterODT = Exporter<
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema,
|
||||
React.ReactNode,
|
||||
React.ReactNode,
|
||||
Record<string, string>,
|
||||
React.ReactNode
|
||||
>;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@blocknote/core';
|
||||
import { Canvg } from 'canvg';
|
||||
import { IParagraphOptions, ShadingType } from 'docx';
|
||||
import React from 'react';
|
||||
|
||||
export function downloadFile(blob: Blob, filename: string) {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
@@ -98,3 +99,76 @@ export function docxBlockPropsToStyles(
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
// ODT helpers
|
||||
type OdtExporterLike = {
|
||||
options?: { colors?: typeof COLORS_DEFAULT };
|
||||
registerStyle: (fn: (name: string) => React.ReactNode) => string;
|
||||
};
|
||||
|
||||
function isOdtExporterLike(value: unknown): value is OdtExporterLike {
|
||||
return (
|
||||
!!value &&
|
||||
typeof (value as { registerStyle?: unknown }).registerStyle === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
export function odtRegisterParagraphStyleForBlock(
|
||||
exporter: unknown,
|
||||
props: Partial<DefaultProps>,
|
||||
options?: { paddingCm?: number; parentStyleName?: string },
|
||||
) {
|
||||
if (!isOdtExporterLike(exporter)) {
|
||||
throw new Error('Invalid ODT exporter: missing registerStyle');
|
||||
}
|
||||
|
||||
const colors = exporter.options?.colors;
|
||||
|
||||
const bgColorHex =
|
||||
props.backgroundColor && props.backgroundColor !== 'default' && colors
|
||||
? colors[props.backgroundColor].background
|
||||
: undefined;
|
||||
|
||||
const textColorHex =
|
||||
props.textColor && props.textColor !== 'default' && colors
|
||||
? colors[props.textColor].text
|
||||
: undefined;
|
||||
|
||||
const foTextAlign =
|
||||
!props.textAlignment || props.textAlignment === 'left'
|
||||
? 'start'
|
||||
: props.textAlignment === 'center'
|
||||
? 'center'
|
||||
: props.textAlignment === 'right'
|
||||
? 'end'
|
||||
: 'justify';
|
||||
|
||||
const paddingCm = options?.paddingCm ?? 0.42; // ~1rem (16px)
|
||||
const parentStyleName = options?.parentStyleName;
|
||||
|
||||
// registerStyle is available on ODT exporter; call through with React elements
|
||||
const styleName = exporter.registerStyle((name: string) =>
|
||||
React.createElement(
|
||||
'style:style',
|
||||
{
|
||||
'style:name': name,
|
||||
'style:family': 'paragraph',
|
||||
...(parentStyleName
|
||||
? { 'style:parent-style-name': parentStyleName }
|
||||
: {}),
|
||||
},
|
||||
React.createElement('style:paragraph-properties', {
|
||||
'fo:text-align': foTextAlign,
|
||||
'fo:padding': `${paddingCm}cm`,
|
||||
...(bgColorHex ? { 'fo:background-color': bgColorHex } : {}),
|
||||
}),
|
||||
textColorHex
|
||||
? React.createElement('style:text-properties', {
|
||||
'fo:color': textColorHex,
|
||||
})
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
return styleName;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('DocToolBox - Licence', () => {
|
||||
await userEvent.click(optionsButton);
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'Download your document in a .docx or .pdf format.',
|
||||
'Download your document in a .docx, .odt or .pdf format.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
}, 10000);
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Pellgargañ",
|
||||
"Download anyway": "Pellgargañ memestra",
|
||||
"Download your document in a .docx or .pdf format.": "Pellgargañ ho restr dindan ur stumm .docx pe .pdf.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Pellgargañ ho restr dindan ur stumm .docx, .odt pe .pdf.",
|
||||
"Duplicate": "Eilañ",
|
||||
"Editing": "Oc'h aozañ",
|
||||
"Editor": "Embanner",
|
||||
@@ -296,7 +296,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Herunterladen",
|
||||
"Download anyway": "Trotzdem herunterladen",
|
||||
"Download your document in a .docx or .pdf format.": "Ihr Dokument als .docx- oder .pdf-Datei herunterladen.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Ihr Dokument als .docx-, .odt- oder .pdf-Datei herunterladen.",
|
||||
"Duplicate": "Duplizieren",
|
||||
"Editing": "Bearbeiten",
|
||||
"Editor": "Mitbearbeiter",
|
||||
@@ -494,7 +494,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Descargar",
|
||||
"Download anyway": "Descargar de todos modos",
|
||||
"Download your document in a .docx or .pdf format.": "Descargue su documento en formato .docx o .pdf.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Descargue su documento en formato .docx, .odt o .pdf.",
|
||||
"Editor": "Editor",
|
||||
"Editor unavailable": "Editor no disponible",
|
||||
"Emojify": "Emojizar",
|
||||
@@ -698,7 +698,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Télécharger",
|
||||
"Download anyway": "Télécharger malgré tout",
|
||||
"Download your document in a .docx or .pdf format.": "Téléchargez votre document au format .docx ou .pdf.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Téléchargez votre document au format .docx, .odt ou .pdf.",
|
||||
"Drag and drop status": "État du glisser-déposer",
|
||||
"Duplicate": "Dupliquer",
|
||||
"Edit document emoji": "Modifier l'émoticône du document",
|
||||
@@ -930,7 +930,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Scarica",
|
||||
"Download anyway": "Scarica comunque",
|
||||
"Download your document in a .docx or .pdf format.": "Scarica il tuo documento in formato .docx o .pdf",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Scarica il tuo documento in formato .docx, .odt o .pdf",
|
||||
"Editor": "Editor",
|
||||
"Editor unavailable": "Editor non disponibile",
|
||||
"Emojify": "Emojify",
|
||||
@@ -1117,7 +1117,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Download",
|
||||
"Download anyway": "Download alsnog",
|
||||
"Download your document in a .docx or .pdf format.": "Download jouw document in .docx of .pdf formaat.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Download jouw document in .docx, .odt of .pdf formaat.",
|
||||
"Drag and drop status": "Drag & drop status",
|
||||
"Duplicate": "Dupliceer",
|
||||
"Edit document emoji": "Bewerk document emoji",
|
||||
@@ -1394,7 +1394,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Загрузить",
|
||||
"Download anyway": "Всё равно загрузить",
|
||||
"Download your document in a .docx or .pdf format.": "Загрузите свой документ в формате .docx или .pdf.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Загрузите свой документ в формате .docx, .odt или .pdf.",
|
||||
"Drag and drop status": "Состояние перетаскивания",
|
||||
"Duplicate": "Дублировать",
|
||||
"Editing": "Редактирование",
|
||||
@@ -1642,7 +1642,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "İndir",
|
||||
"Download anyway": "Yine de indir",
|
||||
"Download your document in a .docx or .pdf format.": "Belgenizi .docx veya .pdf formatında indirin.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Belgenizi .docx, .odt veya .pdf formatında indirin.",
|
||||
"Editor": "Editör",
|
||||
"Editor unavailable": "Editör mevcut değil",
|
||||
"Emojify": "Emojileştir",
|
||||
@@ -1774,7 +1774,7 @@
|
||||
"Docx": "Docx",
|
||||
"Download": "Завантажити",
|
||||
"Download anyway": "Все одно завантажити",
|
||||
"Download your document in a .docx or .pdf format.": "Завантажте ваш документ у форматі .docx або .pdf.",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "Завантажте ваш документ у форматі .docx, .odt або .pdf.",
|
||||
"Drag and drop status": "Стан перетягування",
|
||||
"Duplicate": "Дублювати",
|
||||
"Editing": "Редагування",
|
||||
@@ -2018,7 +2018,7 @@
|
||||
"Docx": "Doc",
|
||||
"Download": "下载",
|
||||
"Download anyway": "仍要下载",
|
||||
"Download your document in a .docx or .pdf format.": "以doc或者pdf格式下载。",
|
||||
"Download your document in a .docx, .odt or .pdf format.": "以doc, odt或者pdf格式下载。",
|
||||
"Editing": "正在编辑",
|
||||
"Editor": "编辑者",
|
||||
"Editor unavailable": "编辑功能不可用",
|
||||
|
||||
@@ -1192,6 +1192,17 @@
|
||||
prosemirror-view "^1.41.2"
|
||||
react-icons "^5.2.1"
|
||||
|
||||
"@blocknote/xl-odt-exporter@0.41.1":
|
||||
version "0.41.1"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/xl-odt-exporter/-/xl-odt-exporter-0.41.1.tgz#55be888d7b6158a6e352aab414cee12e1dcf4326"
|
||||
integrity sha512-VAQC8isRoioK097yuFX0p6dIrwp/GyWInd4hDkux3gsGTMqdXRiLV42symC6+qEseukz+IbGqWvWGsgAplwkZQ==
|
||||
dependencies:
|
||||
"@blocknote/core" "0.41.1"
|
||||
"@blocknote/xl-multi-column" "0.41.1"
|
||||
"@zip.js/zip.js" "^2.7.57"
|
||||
buffer "^6.0.3"
|
||||
image-meta "^0.2.1"
|
||||
|
||||
"@blocknote/xl-pdf-exporter@0.41.1":
|
||||
version "0.41.1"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/xl-pdf-exporter/-/xl-pdf-exporter-0.41.1.tgz#b55c7e8c6a21ae069a42671b1391eab9d4119195"
|
||||
@@ -6253,6 +6264,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||
|
||||
"@zip.js/zip.js@^2.7.57":
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.8.10.tgz#98a0cc7fdef9d6e227236271af412db02b18a5b2"
|
||||
integrity sha512-WVywWK8HSttmFFYSih7lUjjaV4zGzMxy992y0tHrZY4Wf9x/uNBA/XJ50RvfGjuuJKti4yueEHA2ol2pOq6VDg==
|
||||
|
||||
abs-svg-path@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf"
|
||||
|
||||
Reference in New Issue
Block a user