From 44ea96a4263c99f3374577ebab88e75637e07429 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 20 Nov 2025 15:42:52 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20fix=20toolbar=20not=20?= =?UTF-8?q?activated=20when=20reader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When user was a reader of the document, the toolbar of the BlockNote editor was not activated, making it impossible to download resources like images. We add the toolbar even in viewer mode. We block as well automatic document mutation from custom blocks when the editor is in viewer mode to avoid unwanted modifications. --- CHANGELOG.md | 10 ++- .../__tests__/app-impress/doc-editor.spec.ts | 64 ++++++++++++++++--- .../doc-editor/components/BlockNoteEditor.tsx | 6 +- .../custom-blocks/UploadLoaderBlock.tsx | 9 ++- .../InterlinkingLinkInlineContent.tsx | 11 +++- .../Interlinking/SearchPage.tsx | 9 +++ 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3b5e6f02..f610990f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,17 @@ and this project adheres to ## [Unreleased] +### Added + +- ✨ Add comments feature to the editor #1330 +- ✨(backend) Comments on text editor #1330 + ### Fixed - ♿(frontend) improve accessibility: - ♿(frontend) improve share modal button accessibility #1626 +- 🐛(frontend) fix toolbar not activated when reader #1640 +- 🐛(frontend) preserve left panel width on window resize #1588 ## [3.10.0] - 2025-11-18 @@ -40,7 +47,6 @@ and this project adheres to ### Security - mitigate role escalation in the ask_for_access viewset #1580 -- 🐛(frontend) preserve left panel width on window resize #1588 ### Removed @@ -54,7 +60,6 @@ and this project adheres to - ✨(frontend) create skeleton component for DocEditor #1491 - ✨(frontend) add an EmojiPicker in the document tree and title #1381 - ✨(frontend) ajustable left panel #1456 -- ✨ Add comments feature to the editor #1330 ### Changed @@ -183,7 +188,6 @@ and this project adheres to ### Added -- ✨(backend) Comments on text editor #1309 - 👷(CI) add bundle size check job #1268 - ✨(frontend) use title first emoji as doc icon in tree #1289 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index 47728dfca1..15a86db5fc 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -241,20 +241,66 @@ test.describe('Doc Editor', () => { await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible(); }); - test('it cannot edit if viewer', async ({ page }) => { - await mockedDocument(page, { - user_role: 'reader', - }); + test('it cannot edit if viewer but see and can get resources', async ({ + page, + browserName, + }) => { + const [docTitle] = await createDoc(page, 'doc-viewer', browserName, 1); + await verifyDocName(page, docTitle); - await goToGridDoc(page); + await writeInEditor({ page, text: 'Hello World' }); - const card = page.getByLabel('It is the card information'); - await expect(card).toBeVisible(); + await page.getByRole('button', { name: 'Share' }).click(); + await updateShareLink(page, 'Public', 'Reading'); + + // Close the modal + await page.getByRole('button', { name: 'close' }).first().click(); - await expect(card.getByText('Reader')).toBeVisible(); + const { otherPage, cleanup } = await connectOtherUserToDoc({ + browserName, + docUrl: page.url(), + withoutSignIn: true, + docTitle, + }); - const editor = page.locator('.ProseMirror'); + await expect( + otherPage.getByLabel('It is the card information').getByText('Reader'), + ).toBeVisible(); + + // Cannot edit + const editor = otherPage.locator('.ProseMirror'); await expect(editor).toHaveAttribute('contenteditable', 'false'); + + // Owner add a image + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.locator('.bn-block-outer').last().fill('/'); + await page.getByText('Resizable image with caption').click(); + await page.getByText('Upload image').click(); + + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( + path.join(__dirname, 'assets/logo-suite-numerique.png'), + ); + + // Owner see the image + await expect( + page.locator('.--docs--editor-container img.bn-visual-media').first(), + ).toBeVisible(); + + // Viewser see the image + const viewerImg = otherPage + .locator('.--docs--editor-container img.bn-visual-media') + .first(); + await expect(viewerImg).toBeVisible(); + + // Viewer can download the image + await viewerImg.click(); + const downloadPromise = otherPage.waitForEvent('download'); + await otherPage.getByRole('button', { name: 'Download image' }).click(); + const download = await downloadPromise; + expect(download.suggestedFilename()).toBe('logo-suite-numerique.png'); + + await cleanup(); }); test('it adds an image to the doc editor', async ({ page, browserName }) => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 19e7991676..1c8b270115 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -289,11 +289,13 @@ export const BlockNoteReader = ({ editor={editor} editable={false} theme="light" - aria-label={t('Document version viewer')} + aria-label={t('Document viewer')} formattingToolbar={false} slashMenu={false} comments={false} - /> + > + + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx index 3e30224b73..0934a23ec9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx @@ -59,9 +59,14 @@ const UploadLoaderBlockComponent = ({ editor, }: UploadLoaderBlockComponentProps) => { const mediaUrl = useMediaUrl(); + const isEditable = editor.isEditable; useEffect(() => { - if (!block.props.blockUploadUrl || block.props.type !== 'loading') { + if ( + !block.props.blockUploadUrl || + block.props.type !== 'loading' || + !isEditable + ) { return; } @@ -108,7 +113,7 @@ const UploadLoaderBlockComponent = ({ /* During collaboration, another user might have updated the block */ } }); - }, [block, editor, mediaUrl]); + }, [block, editor, mediaUrl, isEditable]); return ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx index 28166de12d..846fa54abf 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx @@ -26,14 +26,19 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec( content: 'none', }, { - render: ({ inlineContent, updateInlineContent }) => { + render: ({ editor, inlineContent, updateInlineContent }) => { const { data: doc } = useDoc({ id: inlineContent.props.docId }); + const isEditable = editor.isEditable; /** * Update the content title if the referenced doc title changes */ useEffect(() => { - if (doc?.title && doc.title !== inlineContent.props.title) { + if ( + isEditable && + doc?.title && + doc.title !== inlineContent.props.title + ) { updateInlineContent({ type: 'interlinkingLinkInline', props: { @@ -50,7 +55,7 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec( * not when inlineContent.props.title changes. */ // eslint-disable-next-line react-hooks/exhaustive-deps - }, [doc?.title]); + }, [doc?.title, isEditable]); return ; }, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx index b6a23b6b08..2edebee606 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx @@ -86,6 +86,7 @@ export const SearchPage = ({ const [search, setSearch] = useState(''); const { isDesktop } = useResponsiveStore(); const { untitledDocument } = useTrans(); + const isEditable = editor.isEditable; /** * createReactInlineContentSpec add automatically the focus after @@ -101,6 +102,10 @@ export const SearchPage = ({ }, [inputRef]); const closeSearch = (insertContent: string) => { + if (!isEditable) { + return; + } + updateInlineContent({ type: 'interlinkingSearchInline', props: { @@ -223,6 +228,10 @@ export const SearchPage = ({ search={search} filters={{ target: DocSearchTarget.CURRENT }} onSelect={(doc) => { + if (!isEditable) { + return; + } + updateInlineContent({ type: 'interlinkingSearchInline', props: {