From c1566d98fe0325ef4d9d80a3c7d5cec3777f6808 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 7 Aug 2024 14:45:02 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20export=20to=20do?= =?UTF-8?q?cx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can now export the document to docx format. We adapted the frontend to be able to choose between pdf or docx export. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-export.spec.ts | 240 ++++++++++++++ .../__tests__/app-impress/doc-header.spec.ts | 192 ++++++++++- .../__tests__/app-impress/doc-tools.spec.ts | 301 ------------------ src/frontend/apps/e2e/package.json | 1 + .../api/{useCreatePdf.tsx => useExport.tsx} | 20 +- .../docs/doc-header/components/DocToolBox.tsx | 6 +- .../{ModalPDF.tsx => ModalExport.tsx} | 52 ++- .../src/features/docs/doc-header/utils.ts | 123 ++++++- src/frontend/yarn.lock | 152 ++++++++- 10 files changed, 748 insertions(+), 340 deletions(-) create mode 100644 src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts delete mode 100644 src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts rename src/frontend/apps/impress/src/features/docs/doc-header/api/{useCreatePdf.tsx => useExport.tsx} (55%) rename src/frontend/apps/impress/src/features/docs/doc-header/components/{ModalPDF.tsx => ModalExport.tsx} (76%) 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==