diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1311ce7..6e209fac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to
- 🎨(frontend) better conversion editor to pdf #151
- ✨(frontend) Versioning #147
+- ✨Export docx (word) #161
## Fixed
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
new file mode 100644
index 00000000..6ff83ab9
--- /dev/null
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
@@ -0,0 +1,240 @@
+import { expect, test } from '@playwright/test';
+import cs from 'convert-stream';
+import jsdom from 'jsdom';
+import pdf from 'pdf-parse';
+
+import { createDoc } from './common';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+});
+
+test.describe('Doc Export', () => {
+ test('it converts the doc to pdf with a template integrated', async ({
+ page,
+ browserName,
+ }) => {
+ const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
+
+ const downloadPromise = page.waitForEvent('download', (download) => {
+ return download.suggestedFilename().includes(`${randomDoc}.pdf`);
+ });
+
+ await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
+
+ await page.locator('.ProseMirror.bn-editor').click();
+ await page.locator('.ProseMirror.bn-editor').fill('Hello World');
+
+ await page.getByLabel('Open the document options').click();
+ await page
+ .getByRole('button', {
+ name: 'Export',
+ })
+ .click();
+
+ await page
+ .getByRole('button', {
+ name: 'Download',
+ })
+ .click();
+
+ const download = await downloadPromise;
+ expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
+
+ const pdfBuffer = await cs.toBuffer(await download.createReadStream());
+ const pdfText = (await pdf(pdfBuffer)).text;
+
+ expect(pdfText).toContain('Hello World'); // This is the doc text
+ });
+
+ test('it converts the doc to docx with a template integrated', async ({
+ page,
+ browserName,
+ }) => {
+ const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
+
+ const downloadPromise = page.waitForEvent('download', (download) => {
+ return download.suggestedFilename().includes(`${randomDoc}.docx`);
+ });
+
+ await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
+
+ await page.locator('.ProseMirror.bn-editor').click();
+ await page.locator('.ProseMirror.bn-editor').fill('Hello World');
+
+ await page.getByLabel('Open the document options').click();
+ await page
+ .getByRole('button', {
+ name: 'Export',
+ })
+ .click();
+
+ await page.getByText('Docx').click();
+
+ await page
+ .getByRole('button', {
+ name: 'Download',
+ })
+ .click();
+
+ const download = await downloadPromise;
+ expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
+ });
+
+ test('it converts the blocknote json in correct html for the export', async ({
+ page,
+ browserName,
+ }) => {
+ const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
+ let body = '';
+
+ await page.route('**/templates/*/generate-document/', async (route) => {
+ const request = route.request();
+ body = request.postDataJSON().body;
+
+ await route.continue();
+ });
+
+ await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
+
+ await page.locator('.bn-block-outer').last().fill('Hello World');
+ await page.locator('.bn-block-outer').last().click();
+ await page.keyboard.press('Enter');
+ await page.keyboard.press('Enter');
+ await page.locator('.bn-block-outer').last().fill('Break');
+ await expect(page.getByText('Break')).toBeVisible();
+
+ // Center the text
+ await page.getByText('Break').dblclick();
+ await page.locator('button[data-test="alignTextCenter"]').click();
+
+ // Change the background color
+ await page.getByText('Break').dblclick();
+ await page.locator('button[data-test="colors"]').click();
+ await page.locator('button[data-test="background-color-brown"]').click();
+
+ // Change the text color
+ await page.getByText('Break').dblclick();
+ await page.locator('button[data-test="colors"]').click();
+ await page.locator('button[data-test="text-color-orange"]').click();
+
+ // Add a list
+ await page.locator('.bn-block-outer').last().click();
+ await page.keyboard.press('Enter');
+ await page.locator('.bn-block-outer').last().fill('Test List 1');
+ await page.getByText('Test List 1').dblclick();
+ await page
+ .getByRole('button', {
+ name: 'Paragraph',
+ })
+ .click();
+ await page
+ .getByRole('menuitem', {
+ name: 'Bullet List',
+ })
+ .click();
+ await page
+ .locator('.bn-block-content[data-content-type="bulletListItem"]')
+ .last()
+ .click();
+ await page.keyboard.press('Enter');
+ await page
+ .locator('.bn-block-content[data-content-type="bulletListItem"] p')
+ .last()
+ .fill('Test List 2');
+ await page.keyboard.press('Enter');
+ await page
+ .locator('.bn-block-content[data-content-type="bulletListItem"] p')
+ .last()
+ .fill('Test List 3');
+
+ // Add a number list
+ await page.locator('.bn-block-outer').last().click();
+ await page.keyboard.press('Enter');
+ await page.locator('.bn-block-outer').last().fill('Test Number 1');
+ await page.getByText('Test Number 1').dblclick();
+ await page
+ .getByRole('button', {
+ name: 'Paragraph',
+ })
+ .click();
+ await page
+ .getByRole('menuitem', {
+ name: 'Numbered List',
+ })
+ .click();
+ await page
+ .locator('.bn-block-content[data-content-type="numberedListItem"]')
+ .last()
+ .click();
+ await page.keyboard.press('Enter');
+ await page
+ .locator('.bn-block-content[data-content-type="numberedListItem"] p')
+ .last()
+ .fill('Test Number 2');
+ await page.keyboard.press('Enter');
+ await page
+ .locator('.bn-block-content[data-content-type="numberedListItem"] p')
+ .last()
+ .fill('Test Number 3');
+
+ // Add img
+ await page.locator('.bn-block-outer').last().click();
+ await page.keyboard.press('Enter');
+ await page.locator('.bn-block-outer').last().fill('/');
+ await page
+ .getByRole('option', {
+ name: 'Image',
+ })
+ .click();
+ await page
+ .getByPlaceholder('Enter URL')
+ .fill('https://example.com/image.jpg');
+ await page
+ .getByRole('button', {
+ name: 'Embed image',
+ })
+ .click();
+
+ // Download
+ await page.getByLabel('Open the document options').click();
+ await page
+ .getByRole('button', {
+ name: 'Export',
+ })
+ .click();
+
+ await page
+ .getByRole('button', {
+ name: 'Download',
+ })
+ .click();
+
+ // Empty paragraph should be replaced by a
+ expect(body.match(/
/g)?.length).toBeGreaterThanOrEqual(2);
+ expect(body).toContain('style="color: orange;"');
+ expect(body).toContain('custom-style="center"');
+ expect(body).toContain('style="background-color: brown;"');
+
+ const { JSDOM } = jsdom;
+ const DOMParser = new JSDOM().window.DOMParser;
+ const parser = new DOMParser();
+ const html = parser.parseFromString(body, 'text/html');
+
+ const ulLis = html.querySelectorAll('ul li');
+ expect(ulLis.length).toBe(3);
+ expect(ulLis[0].textContent).toBe('Test List 1');
+ expect(ulLis[1].textContent).toBe('Test List 2');
+ expect(ulLis[2].textContent).toBe('Test List 3');
+
+ const olLis = html.querySelectorAll('ol li');
+ expect(olLis.length).toBe(3);
+ expect(olLis[0].textContent).toBe('Test Number 1');
+ expect(olLis[1].textContent).toBe('Test Number 2');
+ expect(olLis[2].textContent).toBe('Test Number 3');
+
+ const img = html.querySelectorAll('img');
+ expect(img.length).toBe(1);
+ expect(img[0].src).toBe('https://example.com/image.jpg');
+ });
+});
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
index eb6dfa51..839d6cd1 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
-import { goToGridDoc, mockedDocument } from './common';
+import { createDoc, goToGridDoc, mockedDocument } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -63,4 +63,194 @@ test.describe('Doc Header', () => {
await expect(card.getByText('Your role: Owner')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
});
+
+ test('it updates the doc', async ({ page, browserName }) => {
+ const [randomDoc] = await createDoc(
+ page,
+ 'doc-update',
+ browserName,
+ 1,
+ true,
+ );
+ await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
+
+ await page.getByLabel('Open the document options').click();
+ await page
+ .getByRole('button', {
+ name: 'Update document',
+ })
+ .click();
+
+ await expect(
+ page.locator('h2').getByText(`Update document "${randomDoc}"`),
+ ).toBeVisible();
+
+ await expect(
+ page.getByRole('checkbox', { name: 'Is it public ?' }),
+ ).toBeChecked();
+
+ await page.getByText('Document name').fill(`${randomDoc}-updated`);
+ await page.getByText('Is it public ?').click();
+
+ await page
+ .getByRole('button', {
+ name: 'Validate the modification',
+ })
+ .click();
+
+ await expect(
+ page.getByText('The document has been updated.'),
+ ).toBeVisible();
+
+ const docTitle = await goToGridDoc(page, {
+ title: `${randomDoc}-updated`,
+ });
+
+ await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
+
+ await page.getByLabel('Open the document options').click();
+ await page
+ .getByRole('button', {
+ name: 'Update document',
+ })
+ .click();
+
+ await expect(
+ page.getByRole('checkbox', { name: 'Is it public ?' }),
+ ).not.toBeChecked();
+ });
+
+ test('it deletes the doc', async ({ page, browserName }) => {
+ const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
+ await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
+
+ await page.getByLabel('Open the document options').click();
+ await page
+ .getByRole('button', {
+ name: 'Delete document',
+ })
+ .click();
+
+ await expect(
+ page.locator('h2').getByText(`Deleting the document "${randomDoc}"`),
+ ).toBeVisible();
+
+ await page
+ .getByRole('button', {
+ name: 'Confirm deletion',
+ })
+ .click();
+
+ await expect(
+ page.getByText('The document has been deleted.'),
+ ).toBeVisible();
+
+ await expect(
+ page.getByRole('button', { name: 'Create a new document' }),
+ ).toBeVisible();
+
+ const row = page
+ .getByLabel('Datagrid of the documents page 1')
+ .getByRole('table')
+ .getByRole('row')
+ .filter({
+ hasText: randomDoc,
+ });
+
+ expect(await row.count()).toBe(0);
+ });
+
+ test('it checks the options available if administrator', async ({ page }) => {
+ await mockedDocument(page, {
+ abilities: {
+ destroy: false, // Means not owner
+ versions_destroy: true,
+ versions_list: true,
+ versions_retrieve: true,
+ manage_accesses: true, // Means admin
+ update: true,
+ partial_update: true,
+ retrieve: true,
+ },
+ });
+
+ await goToGridDoc(page);
+
+ await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
+
+ await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
+
+ await page.getByLabel('Open the document options').click();
+
+ await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Update document' }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Delete document' }),
+ ).toBeHidden();
+ });
+
+ test('it checks the options available if editor', async ({ page }) => {
+ await mockedDocument(page, {
+ abilities: {
+ destroy: false, // Means not owner
+ versions_destroy: true,
+ versions_list: true,
+ versions_retrieve: true,
+ manage_accesses: false, // Means not admin
+ update: true,
+ partial_update: true, // Means editor
+ retrieve: true,
+ },
+ });
+
+ await goToGridDoc(page);
+
+ await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
+
+ await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
+
+ await page.getByLabel('Open the document options').click();
+
+ await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Update document' }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Delete document' }),
+ ).toBeHidden();
+ });
+
+ test('it checks the options available if reader', async ({ page }) => {
+ await mockedDocument(page, {
+ abilities: {
+ destroy: false, // Means not owner
+ versions_destroy: false,
+ versions_list: true,
+ versions_retrieve: true,
+ manage_accesses: false, // Means not admin
+ update: false,
+ partial_update: false, // Means not editor
+ retrieve: true,
+ },
+ });
+
+ await goToGridDoc(page);
+
+ await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
+
+ await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
+
+ await page.getByLabel('Open the document options').click();
+
+ await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
+ await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: 'Update document' }),
+ ).toBeHidden();
+ await expect(
+ page.getByRole('button', { name: 'Delete document' }),
+ ).toBeHidden();
+ });
});
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts
deleted file mode 100644
index eb3096ca..00000000
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-import { expect, test } from '@playwright/test';
-import cs from 'convert-stream';
-import pdf from 'pdf-parse';
-
-import { createDoc, goToGridDoc, mockedDocument } from './common';
-
-test.beforeEach(async ({ page }) => {
- await page.goto('/');
-});
-
-test.describe('Doc Tools', () => {
- test('it converts the doc to pdf with a template integrated', async ({
- page,
- browserName,
- }) => {
- const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
-
- const downloadPromise = page.waitForEvent('download', (download) => {
- return download.suggestedFilename().includes(`${randomDoc}.pdf`);
- });
-
- await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
-
- await page.locator('.ProseMirror.bn-editor').click();
- await page.locator('.ProseMirror.bn-editor').fill('Hello World');
-
- await page.getByLabel('Open the document options').click();
- await page
- .getByRole('button', {
- name: 'Generate PDF',
- })
- .click();
-
- await page
- .getByRole('button', {
- name: 'Download',
- })
- .click();
-
- const download = await downloadPromise;
- expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
-
- const pdfBuffer = await cs.toBuffer(await download.createReadStream());
- const pdfText = (await pdf(pdfBuffer)).text;
-
- expect(pdfText).toContain('Hello World'); // This is the doc text
- });
-
- test('it converts the blocknote json in correct html for the pdf', async ({
- page,
- browserName,
- }) => {
- const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
- let body = '';
-
- await page.route('**/templates/*/generate-document/', async (route) => {
- const request = route.request();
- body = request.postDataJSON().body;
-
- await route.continue();
- });
-
- await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
-
- await page.locator('.bn-block-outer').last().fill('Hello World');
- await page.locator('.bn-block-outer').last().click();
- await page.keyboard.press('Enter');
- await page.keyboard.press('Enter');
- await page.locator('.bn-block-outer').last().fill('Break');
- await expect(page.getByText('Break')).toBeVisible();
-
- // Center the text
- await page.getByText('Break').dblclick();
- await page.locator('button[data-test="alignTextCenter"]').click();
-
- // Change the background color
- await page.getByText('Break').dblclick();
- await page.locator('button[data-test="colors"]').click();
- await page.locator('button[data-test="background-color-brown"]').click();
-
- // Change the text color
- await page.getByText('Break').dblclick();
- await page.locator('button[data-test="colors"]').click();
- await page.locator('button[data-test="text-color-orange"]').click();
-
- await page.getByLabel('Open the document options').click();
- await page
- .getByRole('button', {
- name: 'Generate PDF',
- })
- .click();
-
- await page
- .getByRole('button', {
- name: 'Download',
- })
- .click();
-
- // Empty paragraph should be replaced by a
- expect(body.match(/
/g)?.length).toBeGreaterThanOrEqual(2);
- expect(body).toContain('style="color: orange;"');
- expect(body).toContain('style="text-align: center;"');
- expect(body).toContain('style="background-color: brown;"');
- });
-
- test('it updates the doc', async ({ page, browserName }) => {
- const [randomDoc] = await createDoc(
- page,
- 'doc-update',
- browserName,
- 1,
- true,
- );
- await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
-
- await page.getByLabel('Open the document options').click();
- await page
- .getByRole('button', {
- name: 'Update document',
- })
- .click();
-
- await expect(
- page.locator('h2').getByText(`Update document "${randomDoc}"`),
- ).toBeVisible();
-
- await expect(
- page.getByRole('checkbox', { name: 'Is it public ?' }),
- ).toBeChecked();
-
- await page.getByText('Document name').fill(`${randomDoc}-updated`);
- await page.getByText('Is it public ?').click();
-
- await page
- .getByRole('button', {
- name: 'Validate the modification',
- })
- .click();
-
- await expect(
- page.getByText('The document has been updated.'),
- ).toBeVisible();
-
- const docTitle = await goToGridDoc(page, {
- title: `${randomDoc}-updated`,
- });
-
- await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
-
- await page.getByLabel('Open the document options').click();
- await page
- .getByRole('button', {
- name: 'Update document',
- })
- .click();
-
- await expect(
- page.getByRole('checkbox', { name: 'Is it public ?' }),
- ).not.toBeChecked();
- });
-
- test('it deletes the doc', async ({ page, browserName }) => {
- const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
- await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
-
- await page.getByLabel('Open the document options').click();
- await page
- .getByRole('button', {
- name: 'Delete document',
- })
- .click();
-
- await expect(
- page.locator('h2').getByText(`Deleting the document "${randomDoc}"`),
- ).toBeVisible();
-
- await page
- .getByRole('button', {
- name: 'Confirm deletion',
- })
- .click();
-
- await expect(
- page.getByText('The document has been deleted.'),
- ).toBeVisible();
-
- await expect(
- page.getByRole('button', { name: 'Create a new document' }),
- ).toBeVisible();
-
- const row = page
- .getByLabel('Datagrid of the documents page 1')
- .getByRole('table')
- .getByRole('row')
- .filter({
- hasText: randomDoc,
- });
-
- expect(await row.count()).toBe(0);
- });
-
- test('it checks the options available if administrator', async ({ page }) => {
- await mockedDocument(page, {
- abilities: {
- destroy: false, // Means not owner
- versions_destroy: true,
- versions_list: true,
- versions_retrieve: true,
- manage_accesses: true, // Means admin
- update: true,
- partial_update: true,
- retrieve: true,
- },
- });
-
- await goToGridDoc(page);
-
- await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
-
- await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
-
- await page.getByLabel('Open the document options').click();
-
- await expect(
- page.getByRole('button', { name: 'Generate PDF' }),
- ).toBeVisible();
- await expect(
- page.getByRole('button', { name: 'Update document' }),
- ).toBeVisible();
- await expect(
- page.getByRole('button', { name: 'Delete document' }),
- ).toBeHidden();
- });
-
- test('it checks the options available if editor', async ({ page }) => {
- await mockedDocument(page, {
- abilities: {
- destroy: false, // Means not owner
- versions_destroy: true,
- versions_list: true,
- versions_retrieve: true,
- manage_accesses: false, // Means not admin
- update: true,
- partial_update: true, // Means editor
- retrieve: true,
- },
- });
-
- await goToGridDoc(page);
-
- await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
-
- await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
-
- await page.getByLabel('Open the document options').click();
-
- await expect(
- page.getByRole('button', { name: 'Generate PDF' }),
- ).toBeVisible();
- await expect(
- page.getByRole('button', { name: 'Update document' }),
- ).toBeVisible();
- await expect(
- page.getByRole('button', { name: 'Delete document' }),
- ).toBeHidden();
- });
-
- test('it checks the options available if reader', async ({ page }) => {
- await mockedDocument(page, {
- abilities: {
- destroy: false, // Means not owner
- versions_destroy: false,
- versions_list: true,
- versions_retrieve: true,
- manage_accesses: false, // Means not admin
- update: false,
- partial_update: false, // Means not editor
- retrieve: true,
- },
- });
-
- await goToGridDoc(page);
-
- await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
-
- await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
-
- await page.getByLabel('Open the document options').click();
-
- await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
- await expect(
- page.getByRole('button', { name: 'Generate PDF' }),
- ).toBeVisible();
- await expect(
- page.getByRole('button', { name: 'Update document' }),
- ).toBeHidden();
- await expect(
- page.getByRole('button', { name: 'Delete document' }),
- ).toBeHidden();
- });
-});
diff --git a/src/frontend/apps/e2e/package.json b/src/frontend/apps/e2e/package.json
index da2e0d9c..4d60e3b8 100644
--- a/src/frontend/apps/e2e/package.json
+++ b/src/frontend/apps/e2e/package.json
@@ -17,6 +17,7 @@
},
"dependencies": {
"convert-stream": "1.0.2",
+ "jsdom": "24.1.1",
"pdf-parse": "^1.1.1"
}
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/api/useCreatePdf.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/api/useExport.tsx
similarity index 55%
rename from src/frontend/apps/impress/src/features/docs/doc-header/api/useCreatePdf.tsx
rename to src/frontend/apps/impress/src/features/docs/doc-header/api/useExport.tsx
index a56d9996..1be23ff5 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/api/useCreatePdf.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/api/useExport.tsx
@@ -2,17 +2,19 @@ import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
-interface CreatePdfParams {
+interface CreateExportParams {
templateId: string;
body: string;
body_type: 'html' | 'markdown';
+ format: 'pdf' | 'docx';
}
-export const createPdf = async ({
+export const createExport = async ({
templateId,
body,
body_type,
-}: CreatePdfParams): Promise => {
+ format,
+}: CreateExportParams): Promise => {
const response = await fetchAPI(
`templates/${templateId}/generate-document/`,
{
@@ -20,19 +22,23 @@ export const createPdf = async ({
body: JSON.stringify({
body,
body_type,
+ format,
}),
},
);
if (!response.ok) {
- throw new APIError('Failed to create the pdf', await errorCauses(response));
+ throw new APIError(
+ 'Failed to export the document',
+ await errorCauses(response),
+ );
}
return await response.blob();
};
-export function useCreatePdf() {
- return useMutation({
- mutationFn: createPdf,
+export function useExport() {
+ return useMutation({
+ mutationFn: createExport,
});
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index ae9c417c..9ea72241 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -10,7 +10,7 @@ import {
ModalUpdateDoc,
} from '@/features/docs/doc-management';
-import { ModalPDF } from './ModalPDF';
+import { ModalPDF } from './ModalExport';
interface DocToolBoxProps {
doc: Doc;
@@ -83,10 +83,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
setIsDropOpen(false);
}}
color="primary-text"
- icon={picture_as_pdf}
+ icon={file_download}
size="small"
>
- {t('Generate PDF')}
+ {t('Export')}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalPDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx
similarity index 76%
rename from src/frontend/apps/impress/src/features/docs/doc-header/components/ModalPDF.tsx
rename to src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx
index eb4d9850..612869c1 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalPDF.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx
@@ -4,6 +4,8 @@ import {
Loader,
Modal,
ModalSize,
+ Radio,
+ RadioGroup,
Select,
VariantType,
useToastProvider,
@@ -15,7 +17,7 @@ import { Box, Text } from '@/components';
import { useDocStore } from '@/features/docs/doc-editor/';
import { Doc } from '@/features/docs/doc-management';
-import { useCreatePdf } from '../api/useCreatePdf';
+import { useExport } from '../api/useExport';
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
import { adaptBlockNoteHTML, downloadFile } from '../utils';
@@ -31,14 +33,14 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
const { toast } = useToastProvider();
const { docsStore } = useDocStore();
const {
- mutate: createPdf,
- data: pdf,
+ mutate: createExport,
+ data: documentGenerated,
isSuccess,
-
isPending,
error,
- } = useCreatePdf();
+ } = useExport();
const [templateIdSelected, setTemplateIdSelected] = useState();
+ const [format, setFormat] = useState<'pdf' | 'docx'>('pdf');
const templateOptions = useMemo(() => {
if (!templates?.pages) {
@@ -73,7 +75,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
}, [error, t]);
useEffect(() => {
- if (!pdf || !isSuccess) {
+ if (!documentGenerated || !isSuccess) {
return;
}
@@ -84,16 +86,21 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s/g, '-');
- downloadFile(pdf, `${title}.pdf`);
+ downloadFile(documentGenerated, `${title}.${format}`);
- toast(t('Your pdf was downloaded succesfully'), VariantType.SUCCESS);
+ toast(
+ t('Your {{format}} was downloaded succesfully', {
+ format,
+ }),
+ VariantType.SUCCESS,
+ );
onClose();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [pdf, isSuccess, t]);
+ }, [documentGenerated, isSuccess, t]);
async function onSubmit() {
- if (!templateIdSelected) {
+ if (!templateIdSelected || !format) {
return;
}
@@ -107,10 +114,11 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
let body = await editor.blocksToFullHTML(editor.document);
body = adaptBlockNoteHTML(body);
- createPdf({
+ createExport({
templateId: templateIdSelected,
body,
body_type: 'html',
+ format,
});
}
@@ -148,20 +156,20 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
picture_as_pdf
- {t('Generate PDF')}
+ {t('Export')}
}
>
{t(
- 'Generate a PDF from your document, it will be inserted in the selected template.',
+ 'Export your document, it will be inserted in the selected template.',
)}
@@ -176,6 +184,22 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
}
/>
+
+ setFormat(evt.target.value as 'pdf')}
+ defaultChecked={true}
+ />
+ setFormat(evt.target.value as 'docx')}
+ />
+
+
{isPending && (
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts
index 8918bc00..de15b30e 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/utils.ts
@@ -10,16 +10,137 @@ export function downloadFile(blob: Blob, filename: string) {
window.URL.revokeObjectURL(url);
}
+const convertToLi = (html: string) => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const divs = doc.querySelectorAll(
+ 'div[data-content-type="bulletListItem"] , div[data-content-type="numberedListItem"]',
+ );
+
+ // Loop through each div and replace it with a li
+ divs.forEach((div) => {
+ // Create a new li element
+ const li = document.createElement('li');
+
+ // Copy the attributes from the div to the li
+ for (let i = 0; i < div.attributes.length; i++) {
+ li.setAttribute(div.attributes[i].name, div.attributes[i].value);
+ }
+
+ // Move all child elements of the div to the li
+ while (div.firstChild) {
+ li.appendChild(div.firstChild);
+ }
+
+ // Replace the div with the li in the DOM
+ if (div.parentNode) {
+ div.parentNode.replaceChild(li, div);
+ }
+ });
+
+ /**
+ * Convert the blocknote content to a simplified version to be
+ * correctly parsed by our pdf and docx parser
+ */
+ const newContent: string[] = [];
+ let currentList: HTMLUListElement | HTMLOListElement | null = null;
+
+ // Iterate over all the children of the bn-block-group
+ doc.body
+ .querySelectorAll('.bn-block-group .bn-block-outer')
+ .forEach((outerDiv) => {
+ const blockContent = outerDiv.querySelector('.bn-block-content');
+
+ if (blockContent) {
+ const contentType = blockContent.getAttribute('data-content-type');
+
+ if (contentType === 'bulletListItem') {
+ // If a list is not started, start a new one
+ if (!currentList) {
+ currentList = document.createElement('ul');
+ }
+
+ currentList.appendChild(blockContent);
+ } else if (contentType === 'numberedListItem') {
+ // If a numbered list is not started, start a new one
+ if (!currentList) {
+ currentList = document.createElement('ol');
+ }
+
+ currentList.appendChild(blockContent);
+ } else {
+ /***
+ * If there is a current list, add it to the new content
+ * It means that the current list has ended
+ */
+ if (currentList) {
+ newContent.push(currentList.outerHTML);
+ }
+
+ currentList = null;
+ newContent.push(outerDiv.outerHTML);
+ }
+ } else {
+ // In case there is no content-type, add the outerDiv as is
+ newContent.push(outerDiv.outerHTML);
+ }
+ });
+
+ return newContent.join('');
+};
+
+const convertToImg = (html: string) => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const divs = doc.querySelectorAll('div[data-content-type="image"]');
+
+ // Loop through each div and replace it with a img
+ divs.forEach((div) => {
+ const img = document.createElement('img');
+
+ // Copy the attributes from the div to the img
+ for (let i = 0; i < div.attributes.length; i++) {
+ img.setAttribute(div.attributes[i].name, div.attributes[i].value);
+
+ if (div.attributes[i].name === 'data-url') {
+ img.setAttribute('src', div.attributes[i].value);
+ }
+
+ if (div.attributes[i].name === 'data-preview-width') {
+ img.setAttribute('width', div.attributes[i].value);
+ }
+ }
+
+ // Move all child elements of the div to the img
+ while (div.firstChild) {
+ img.appendChild(div.firstChild);
+ }
+
+ // Replace the div with the img in the DOM
+ if (div.parentNode) {
+ div.parentNode.replaceChild(img, div);
+ }
+ });
+
+ return doc.body.innerHTML;
+};
+
export const adaptBlockNoteHTML = (html: string) => {
html = html.replaceAll('', '
');
+
+ // custom-style is used by pandoc to convert the style
html = html.replaceAll(
/data-text-alignment=\"([a-z]+)\"/g,
- 'style="text-align: $1;"',
+ 'custom-style="$1"',
);
html = html.replaceAll(/data-text-color=\"([a-z]+)\"/g, 'style="color: $1;"');
html = html.replaceAll(
/data-background-color=\"([a-z]+)\"/g,
'style="background-color: $1;"',
);
+
+ html = convertToLi(html);
+ html = convertToImg(html);
+
return html;
};
diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock
index bbdf6361..bdcafee3 100644
--- a/src/frontend/yarn.lock
+++ b/src/frontend/yarn.lock
@@ -1078,7 +1078,7 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
-"@blocknote/core@*", "@blocknote/core@0.15.3", "@blocknote/core@^0.15.3":
+"@blocknote/core@*", "@blocknote/core@^0.15.3":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.15.3.tgz#775ba61e1d61f0e3651988265c498bc1d573f25e"
integrity sha512-2ZOWpxt4rm5YdH9Gn9YXYQsI9wYMAkFcKkuSpprHGCl7ALk+Iv7Gw+BswttlhKFyYoY/df7dlc9lcZr5zVAWWw==
@@ -1125,7 +1125,7 @@
y-protocols "^1.0.6"
yjs "^13.6.15"
-"@blocknote/mantine@*", "@blocknote/mantine@0.15.3":
+"@blocknote/mantine@*":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.15.3.tgz#040b36df2eb262319850205362adc76be6282b22"
integrity sha512-KEtRffEAjqmIW9sSpDdiEgOhsSZLcI2zV/0hHOe/ZM/01UE/V4Vl7VKicgDd7FUBzn2BoqqxOv0GW1n5mLsfCw==
@@ -1139,7 +1139,7 @@
react-dom "^18"
react-icons "^5.2.1"
-"@blocknote/react@*", "@blocknote/react@0.15.3", "@blocknote/react@^0.15.3":
+"@blocknote/react@*", "@blocknote/react@^0.15.3":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.15.3.tgz#e47486d453a223f5660297721748279cbc71e943"
integrity sha512-lawcNmXIo+dQkd7gZkCbvNmKLNL/3y82q5NT26VNQH0axQSTJ7/Mjiam6A82gCvcPWXx/1ILh6OClx8IV977ag==
@@ -4834,7 +4834,7 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
-"@types/node@*", "@types/node@20.14.13":
+"@types/node@*":
version "20.14.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.13.tgz#bf4fe8959ae1c43bc284de78bd6c01730933736b"
integrity sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==
@@ -4861,7 +4861,7 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
-"@types/react-dom@*", "@types/react-dom@18.3.0":
+"@types/react-dom@*":
version "18.3.0"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
@@ -5255,6 +5255,13 @@ agent-base@6:
dependencies:
debug "4"
+agent-base@^7.0.2, agent-base@^7.1.0:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317"
+ integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==
+ dependencies:
+ debug "^4.3.4"
+
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -6124,7 +6131,7 @@ crelt@^1.0.0:
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
-cross-env@*, cross-env@7.0.3:
+cross-env@*:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
@@ -6230,6 +6237,13 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
+cssstyle@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.0.1.tgz#ef29c598a1e90125c870525490ea4f354db0660a"
+ integrity sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==
+ dependencies:
+ rrweb-cssom "^0.6.0"
+
csstype@3.1.3, csstype@^3.0.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
@@ -6249,6 +6263,14 @@ data-urls@^3.0.2:
whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0"
+data-urls@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde"
+ integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==
+ dependencies:
+ whatwg-mimetype "^4.0.0"
+ whatwg-url "^14.0.0"
+
data-view-buffer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2"
@@ -6309,7 +6331,7 @@ debug@^4.0.0, debug@^4.3.6:
dependencies:
ms "2.1.2"
-decimal.js@^10.4.2:
+decimal.js@^10.4.2, decimal.js@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
@@ -7903,6 +7925,13 @@ html-encoding-sniffer@^3.0.0:
dependencies:
whatwg-encoding "^2.0.0"
+html-encoding-sniffer@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448"
+ integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==
+ dependencies:
+ whatwg-encoding "^3.1.1"
+
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@@ -7949,6 +7978,14 @@ http-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
+http-proxy-agent@^7.0.2:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
+ integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
+ dependencies:
+ agent-base "^7.1.0"
+ debug "^4.3.4"
+
https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@@ -7957,6 +7994,14 @@ https-proxy-agent@^5.0.1:
agent-base "6"
debug "4"
+https-proxy-agent@^7.0.5:
+ version "7.0.5"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2"
+ integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==
+ dependencies:
+ agent-base "^7.0.2"
+ debug "4"
+
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
@@ -8902,6 +8947,33 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
+jsdom@24.1.1:
+ version "24.1.1"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.1.1.tgz#f41df8f4f3b2fbfa7e1bdc5df62c9804fd14a9d0"
+ integrity sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==
+ dependencies:
+ cssstyle "^4.0.1"
+ data-urls "^5.0.0"
+ decimal.js "^10.4.3"
+ form-data "^4.0.0"
+ html-encoding-sniffer "^4.0.0"
+ http-proxy-agent "^7.0.2"
+ https-proxy-agent "^7.0.5"
+ is-potential-custom-element-name "^1.0.1"
+ nwsapi "^2.2.12"
+ parse5 "^7.1.2"
+ rrweb-cssom "^0.7.1"
+ saxes "^6.0.0"
+ symbol-tree "^3.2.4"
+ tough-cookie "^4.1.4"
+ w3c-xmlserializer "^5.0.0"
+ webidl-conversions "^7.0.0"
+ whatwg-encoding "^3.1.1"
+ whatwg-mimetype "^4.0.0"
+ whatwg-url "^14.0.0"
+ ws "^8.18.0"
+ xml-name-validator "^5.0.0"
+
jsdom@^20.0.0:
version "20.0.3"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db"
@@ -9909,6 +9981,11 @@ nth-check@^2.0.1:
dependencies:
boolbase "^1.0.0"
+nwsapi@^2.2.12:
+ version "2.2.12"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8"
+ integrity sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==
+
nwsapi@^2.2.2:
version "2.2.10"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8"
@@ -10094,7 +10171,7 @@ parse5@^6.0.0:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
-parse5@^7.0.0, parse5@^7.1.1:
+parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
@@ -10505,7 +10582,7 @@ punycode.js@^2.3.1:
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
-punycode@^2.1.0, punycode@^2.1.1:
+punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@@ -11227,6 +11304,16 @@ rope-sequence@^1.3.0:
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
+rrweb-cssom@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1"
+ integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==
+
+rrweb-cssom@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b"
+ integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==
+
rsvp@^4.8.2:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -12008,7 +12095,7 @@ touch@^3.1.0:
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
-tough-cookie@^4.1.2:
+tough-cookie@^4.1.2, tough-cookie@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
@@ -12032,6 +12119,13 @@ tr46@^3.0.0:
dependencies:
punycode "^2.1.1"
+tr46@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec"
+ integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==
+ dependencies:
+ punycode "^2.3.1"
+
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@@ -12199,7 +12293,7 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
-typescript@*, typescript@5.5.4, typescript@^5.0.4:
+typescript@*, typescript@^5.0.4:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
@@ -12593,6 +12687,13 @@ w3c-xmlserializer@^4.0.0:
dependencies:
xml-name-validator "^4.0.0"
+w3c-xmlserializer@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
+ integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
+ dependencies:
+ xml-name-validator "^5.0.0"
+
walk-sync@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-2.2.0.tgz#80786b0657fcc8c0e1c0b1a042a09eae2966387a"
@@ -12652,11 +12753,23 @@ whatwg-encoding@^2.0.0:
dependencies:
iconv-lite "0.6.3"
+whatwg-encoding@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
+ integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
+ dependencies:
+ iconv-lite "0.6.3"
+
whatwg-mimetype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
+whatwg-mimetype@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
+ integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
+
whatwg-url@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
@@ -12665,6 +12778,14 @@ whatwg-url@^11.0.0:
tr46 "^3.0.0"
webidl-conversions "^7.0.0"
+whatwg-url@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6"
+ integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==
+ dependencies:
+ tr46 "^5.0.0"
+ webidl-conversions "^7.0.0"
+
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@@ -12968,7 +13089,7 @@ write-file-atomic@^5.0.1:
imurmurhash "^0.1.4"
signal-exit "^4.0.1"
-ws@8.18.0, ws@^8.11.0, ws@^8.14.2:
+ws@8.18.0, ws@^8.11.0, ws@^8.14.2, ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
@@ -12978,6 +13099,11 @@ xml-name-validator@^4.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
+xml-name-validator@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
+ integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
+
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
@@ -13046,7 +13172,7 @@ yargs@17.7.2, yargs@^17.3.1:
y18n "^5.0.5"
yargs-parser "^21.1.1"
-yjs@*, yjs@13.6.18, yjs@^13.6.15:
+yjs@*, yjs@^13.6.15:
version "13.6.18"
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.18.tgz#d1575203478bc99ad1b89c098e7d4bacb7f91c3b"
integrity sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==