From e91f9dfc91ed8745cd905fb81b04756132260389 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 1 Sep 2026 12:05:15 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20unify=20toolbox?= =?UTF-8?q?=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depend the part of the app, we had different implementations of the toolbox menu. This commit unifies the implementation and uses the same component for both the docs grid, the doc tree and the doc header. It will make it easier to maintain and add new features to the toolbox menu. --- CHANGELOG.md | 1 + .../e2e/__tests__/app-impress/doc-ai.spec.ts | 13 +- .../__tests__/app-impress/doc-export.spec.ts | 4 +- .../app-impress/doc-grid-move.spec.ts | 13 +- .../__tests__/app-impress/doc-grid.spec.ts | 18 +- .../__tests__/app-impress/doc-header.spec.ts | 106 +------ .../app-impress/doc-member-list.spec.ts | 2 + .../app-impress/doc-trashbin.spec.ts | 10 +- .../__tests__/app-impress/doc-tree.spec.ts | 107 ++++--- .../app-impress/presenter-mode.spec.ts | 13 +- .../e2e/__tests__/app-impress/utils-common.ts | 18 +- .../doc-header/components/DocFloatingBar.tsx | 4 +- .../api/useCreateFavoriteDoc.tsx | 6 +- .../api/useDeleteFavoriteDoc.tsx | 6 +- .../doc-management/components/DocToolBox.tsx | 92 ++++-- .../docs/doc-share/api/useLeaveDoc.ts | 5 +- .../docs/doc-share/api/useUpdateDocLink.tsx | 13 + .../components/DocTreeItemActions.tsx | 283 ++++-------------- .../src/features/docs/doc-tree/utils.ts | 32 ++ .../docs-grid/components/DocsGridActions.tsx | 241 ++------------- .../apps/impress/src/i18n/translations.json | 1 + 21 files changed, 305 insertions(+), 683 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71080ef04..e063bd2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to - ♿️(frontend) announce search loading state for screen readers #2526 - ♻️(frontend) change favorite to star #2539 - 🚚(frontend) add doc move to doc options #2555 +- ♻️(frontend) unified menu #2620 - ♿(frontend) hide decorative emojis in document titles from SR #2527 ### Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts index 33be316f7..273f65288 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts @@ -273,18 +273,9 @@ if (process.env.IS_INSTANCE !== 'true') { title: '', }); - const [randomDoc] = await createDoc( - page, - 'doc-editor-ai', - browserName, - 1, - ); + await createDoc(page, 'doc-editor-ai', browserName, 1); - await verifyDocName(page, randomDoc); - - await page.locator('.bn-block-outer').last().fill('Hello World'); - - const editor = page.locator('.ProseMirror'); + const editor = await writeInEditor({ page, text: 'Hello World' }); await editor.getByText('Hello').selectText(); if (!ai_transform && !ai_translate) { 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 index c6dfbb683..18cca6b39 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts @@ -269,7 +269,9 @@ test.describe('Doc Export', () => { }); await page - .getByRole('button', { name: 'Ouvrir les options du document' }) + .getByRole('button', { + name: `Ouvrir les options du document: ${randomDocFrench}`, + }) .click(); await page.getByRole('menuitem', { name: 'Télécharger' }).click(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts index c57fe0743..8cb102d10 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { + clickInDocOptionMenu, createDoc, getGridRow, getOtherBrowserName, @@ -205,9 +206,7 @@ test.describe('Doc grid move', () => { await expect(docsGrid.getByText(titleDoc2)).toBeVisible(); const row = await getGridRow(page, titleDoc1); - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - - await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); + await clickInDocOptionMenu(page, row, 'Move into a doc'); await expect( page @@ -295,9 +294,7 @@ test.describe('Doc grid move', () => { await expect(docsGrid.getByText(titleDoc2)).toBeVisible(); const row = await getGridRow(page, titleDoc1); - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - - await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); + await clickInDocOptionMenu(page, row, 'Move into a doc'); await expect( page @@ -366,9 +363,7 @@ test.describe('Doc grid move', () => { // The first user should now be able to move the doc await page.reload(); - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - - await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); + await clickInDocOptionMenu(page, row, 'Move into a doc'); await expect( page diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index 98a9133e6..ba627e475 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { + clickInDocOptionMenu, clickInEditorShareButton, createDoc, getGridRow, @@ -103,9 +104,7 @@ test.describe('Document grid item options', () => { await expect(page.getByText(docTitle)).toBeVisible(); const row = await getGridRow(page, docTitle); - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - - await page.getByRole('menuitem', { name: 'Share' }).click(); + await clickInDocOptionMenu(page, row, 'Share'); await expect( page.getByRole('dialog').getByText('Share the document'), @@ -121,8 +120,7 @@ test.describe('Document grid item options', () => { const row = await getGridRow(page, docTitle); // Star - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - await page.getByRole('menuitem', { name: 'Star' }).click(); + await clickInDocOptionMenu(page, row, 'Star'); // Check is starred await expect(row.getByText(/This document is starred/)).toBeVisible(); @@ -133,8 +131,7 @@ test.describe('Document grid item options', () => { await expect(page.getByText(docTitle2)).toBeHidden(); // Unstar - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - await page.getByText('Unstar').click(); + await clickInDocOptionMenu(page, row, 'Unstar'); await expect(row).toBeHidden(); // Check is unstarred @@ -153,8 +150,7 @@ test.describe('Document grid item options', () => { await expect(page.getByText(docTitle)).toBeVisible(); const row = await getGridRow(page, docTitle); - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - await page.getByRole('menuitem', { name: 'Delete' }).click(); + await clickInDocOptionMenu(page, row, 'Delete'); await expect( page.getByRole('heading', { name: 'Delete a doc' }), @@ -186,9 +182,7 @@ test.describe('Document grid item options', () => { ).toBeVisible(); const row = await getGridRow(page, docTitle); - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - - await page.getByRole('menuitem', { name: 'Leave' }).click(); + await clickInDocOptionMenu(page, row, 'Leave'); const modal = page.getByRole('dialog', { name: 'Confirmation to leave the document', 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 a2545999d..b76e7e8eb 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,8 +1,8 @@ import { expect, test } from '@playwright/test'; import { + clickInDocOptionMenu, clickInEditorMenu, - clickInEditorShareButton, createDoc, getGridRow, goToGridDoc, @@ -527,30 +527,23 @@ test.describe('Doc Header', () => { await createDoc(page, `Star doc`, browserName); // Star - await page - .getByRole('button', { name: 'Open the document options' }) - .click(); - await page.getByRole('menuitem', { name: 'Star' }).click(); + await clickInEditorMenu(page, 'Star'); await expect(page.getByText('This document is starred')).toBeVisible(); // UnStar - await page - .getByRole('button', { name: 'Open the document options' }) - .click(); - await page.getByRole('menuitem', { name: 'Unstar' }).click(); + await clickInEditorMenu(page, 'Unstar'); await expect(page.getByText('This document is starred')).toBeHidden(); }); test('it duplicates a document', async ({ page, browserName }) => { const [docTitle] = await createDoc(page, `Duplicate doc`, browserName); - const editor = page.locator('.ProseMirror'); - await editor.click(); - await editor.fill('Hello Duplicated World'); + await writeInEditor({ + page, + text: 'Hello Duplicated World', + }); - await page.getByLabel('Open the document options').click(); - - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await clickInEditorMenu(page, 'Duplicate'); await expect( page.getByText('Document duplicated successfully!'), ).toBeVisible(); @@ -564,10 +557,9 @@ test.describe('Doc Header', () => { await expect(row.getByText(duplicateTitle)).toBeVisible(); - await row.getByRole('button', { name: /Open the menu of actions/ }).click(); - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await clickInDocOptionMenu(page, row, 'Duplicate'); const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle; - await page.getByText(duplicateDuplicateTitle).click(); + await verifyDocName(page, duplicateDuplicateTitle); await expect(page.getByText('Hello Duplicated World')).toBeVisible(); }); @@ -586,6 +578,7 @@ test.describe('Doc Header', () => { const duplicateTitle = 'Copy of ' + childTitle; const docTree = page.getByTestId('doc-tree'); + const currentUrl = page.url(); const child = docTree .getByRole('treeitem') @@ -593,12 +586,9 @@ test.describe('Doc Header', () => { .filter({ hasText: childTitle, }); + await child.hover(); - await child.getByRole('button', { name: /More options/ }).click(); - - const currentUrl = page.url(); - - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await clickInDocOptionMenu(page, child, 'Duplicate'); await expect(page).not.toHaveURL(new RegExp(currentUrl)); @@ -609,73 +599,3 @@ test.describe('Doc Header', () => { ).toBeVisible(); }); }); - -test.describe('Documents Header mobile', () => { - test.use({ viewport: { width: 500, height: 1200 } }); - - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('it checks the copy link button is displayed', async ({ page }) => { - await mockedDocument(page, { - abilities: { - destroy: false, - link_configuration: true, - versions_destroy: true, - versions_list: true, - versions_retrieve: true, - accesses_manage: false, - accesses_view: false, - update: true, - partial_update: true, - retrieve: true, - }, - }); - - await goToGridDoc(page); - - await page.getByLabel('Open the document options').click(); - await expect( - page.getByRole('menuitem', { name: 'Copy link' }), - ).toBeVisible(); - await page.keyboard.press('Escape'); - await page.getByRole('button', { name: 'Share' }).click(); - const shareModal = page.getByRole('dialog', { - name: 'Share the document', - }); - await expect( - shareModal.getByRole('button', { name: 'Copy link' }), - ).toBeVisible(); - }); - - test('it checks the close button on Share modal', async ({ page }) => { - await mockedDocument(page, { - abilities: { - destroy: true, // Means owner - link_configuration: true, - versions_destroy: true, - versions_list: true, - versions_retrieve: true, - accesses_manage: true, - accesses_view: true, - update: true, - partial_update: true, - retrieve: true, - }, - }); - - await goToGridDoc(page); - - await clickInEditorShareButton(page); - - const shareModal = page.getByRole('dialog', { - name: 'Share the document', - }); - await expect(shareModal).toBeVisible(); - await page.getByRole('button', { name: 'close' }).click(); - await expect( - page.getByRole('dialog', { name: 'Share the document' }), - ).toBeHidden(); - }); -}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts index abf7e2673..37d468ccf 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts @@ -188,6 +188,8 @@ test.describe('Document list members', () => { await list.click(); await expect(currentUserRole).toBeVisible(); + await page.waitForTimeout(300); + await newUserRoles.click(); await expect( page.getByRole('menuitemradio', { name: 'Owner' }), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts index 469deb076..8423551bb 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts @@ -1,8 +1,8 @@ import { expect, test } from '@playwright/test'; import { + clickInDocOptionMenu, clickInEditorMenu, - clickInGridMenu, createDoc, getGridRow, verifyDocName, @@ -34,18 +34,18 @@ test.describe('Doc Trashbin', () => { // Delete the first document - Is not displayed const row1 = await getGridRow(page, title1); - await clickInGridMenu(page, row1, 'Delete'); + await clickInDocOptionMenu(page, row1, 'Delete'); await page.getByRole('button', { name: 'Delete document' }).click(); await expect(row1.getByText(title1)).toBeHidden(); // Star the second document - Is displayed in the starred list const row2 = await getGridRow(page, title2); - await clickInGridMenu(page, row2, 'Star'); + await clickInDocOptionMenu(page, row2, 'Star'); await page.getByRole('link', { name: 'Starred', exact: true }).click(); await expect(row2.getByText(title2)).toBeVisible(); // Delete the second document - It is not displayed in the starred list anymore - await clickInGridMenu(page, row2, 'Delete'); + await clickInDocOptionMenu(page, row2, 'Delete'); await page.getByRole('button', { name: 'Delete document' }).click(); await expect(row2.getByText(title2)).toBeHidden(); @@ -86,7 +86,7 @@ test.describe('Doc Trashbin', () => { }), ).toBeDisabled(); - await clickInGridMenu(page, row2, 'Restore'); + await clickInDocOptionMenu(page, row2, 'Restore'); await expect(row2.getByText(title2)).toBeHidden(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts index d6d4cfaaf..5728615dd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { + clickInDocOptionMenu, createDoc, getOtherBrowserName, updateDocTitle, @@ -247,9 +248,7 @@ test.describe('Doc Tree', () => { hasText: docChild, }); await child.hover(); - const menu = child.getByRole('button', { name: /More options/ }); - await menu.click(); - await page.getByText('Move to my docs').click(); + await clickInDocOptionMenu(page, child, 'Move to my docs'); await verifyDocName(page, docParent); @@ -258,24 +257,17 @@ test.describe('Doc Tree', () => { }); test('Only owner can detaches a document', async ({ page, browserName }) => { - await createDoc(page, 'doc-tree-detach', browserName, 1); + const [docParent] = await createDoc( + page, + 'doc-tree-detach', + browserName, + 1, + ); await page.getByRole('button', { name: 'Share' }).click(); const otherBrowserName = getOtherBrowserName(browserName); await addNewMember(page, 0, 'Owner', otherBrowserName); - - const list = page.getByTestId('doc-share-quick-search'); - const currentEmail = - process.env[`SIGN_IN_USERNAME_${browserName.toUpperCase()}`] || ''; - const currentUser = list.getByTestId( - `doc-share-member-row-${currentEmail}`, - ); - const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); - await currentUserRole.click(); - await page.getByRole('menuitemradio', { name: 'Administrator' }).click(); - await list.click(); - await page.getByRole('button', { name: 'Ok' }).click(); const { name: docChild } = await createRootSubPage( @@ -284,12 +276,6 @@ test.describe('Doc Tree', () => { 'doc-tree-detach-child', ); - await expect( - page - .getByLabel('It is the card information about the document.') - .getByText('Administrator ·'), - ).toBeVisible(); - const docTree = page.getByTestId('doc-tree'); await expect(docTree.getByText(docChild)).toBeVisible(); await docTree.click(); @@ -299,13 +285,48 @@ test.describe('Doc Tree', () => { .filter({ hasText: docChild, }); + await child.hover(); - const menu = child.getByRole('button', { name: /More options/ }); + const menu = child.getByRole('button', { + name: /Open the document options/, + }); await menu.click(); await expect( page.getByRole('menuitem', { name: 'Move to my docs' }), - ).toHaveAttribute('aria-disabled', 'true'); + ).toBeVisible(); + + await page.keyboard.press('Escape'); + + await docTree.getByText(docParent).click(); + await verifyDocName(page, docParent); + + // Change the role current user to "Administrator" to test that only the owner can detach a document + await page.getByRole('button', { name: 'Share' }).click(); + const list = page.getByTestId('doc-share-quick-search'); + const currentEmail = + process.env[`SIGN_IN_USERNAME_${browserName.toUpperCase()}`] || ''; + const currentUser = list.getByTestId( + `doc-share-member-row-${currentEmail}`, + ); + const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); + await currentUserRole.click(); + await page.getByRole('menuitemradio', { name: 'Administrator' }).click(); + await list.click(); + await page.getByRole('button', { name: 'Ok' }).click(); + + await expect( + page + .getByLabel('It is the card information about the document.') + .getByText('Administrator ·'), + ).toBeVisible(); + + await child.hover(); + await menu.click(); + + await expect( + page.getByRole('menuitem', { name: 'Move to my docs' }), + ).toBeHidden(); }); test('keyboard navigation with Enter key opens documents', async ({ @@ -371,12 +392,12 @@ test.describe('Doc Tree', () => { await rootItem.focus(); await expect(rootItem).toBeFocused(); - // Press F2 → focus should move to the root actions \"More options\" button + // Press F2 → focus should move to the root actions \"Open the document options\" button await page.keyboard.press('F2'); const rootActions = rootItem.locator('.doc-tree-root-item-actions'); const rootMoreOptionsButton = rootActions.getByRole('button', { - name: /more options/i, + name: /Open the document options/i, }); await expect(rootMoreOptionsButton).toBeFocused(); @@ -386,13 +407,7 @@ test.describe('Doc Tree', () => { page, browserName, }) => { - const [docParent] = await createDoc( - page, - 'doc-tree-shift-tab', - browserName, - 1, - ); - await verifyDocName(page, docParent); + await createDoc(page, 'doc-tree-shift-tab', browserName, 1); const { name: docChild } = await createRootSubPage( page, @@ -400,6 +415,7 @@ test.describe('Doc Tree', () => { 'doc-tree-shift-tab-child', ); + const docTree = page.getByTestId('doc-tree'); const selectedSubDoc = await getTreeRow(page, docChild); await expect(selectedSubDoc).toHaveAttribute('aria-selected', 'true'); @@ -407,7 +423,7 @@ test.describe('Doc Tree', () => { await expect(selectedSubDoc).toBeFocused(); await page.keyboard.press('Tab'); - await expect(page.getByLabel('User menu')).toBeFocused(); + await expect(page.getByLabel('Open user menu')).toBeFocused(); await page.keyboard.press('Tab'); await expect(page.getByLabel('Open help menu')).toBeFocused(); @@ -424,7 +440,7 @@ test.describe('Doc Tree', () => { await expect(page.getByLabel('User menu')).toBeFocused(); await page.keyboard.press('Shift+Tab'); - await expect(selectedSubDoc).toBeFocused(); + await expect(docTree.getByLabel('Root document').first()).toBeFocused(); }); test('it updates the child icon from the tree', async ({ @@ -441,19 +457,6 @@ test.describe('Doc Tree', () => { const row = await getTreeRow(page, docChild); - // Check Remove emoji is not present initially - await row.hover(); - const menu = row.getByRole('button', { name: /More options/ }); - await menu.click(); - await expect( - page.getByRole('menuitem', { name: 'Remove emoji' }), - ).toBeHidden(); - - // Close the menu - await page.keyboard.press('Escape'); - - await page.waitForTimeout(500); - // Update the emoji from the tree await row.locator('.--docs--doc-icon').click(); await page.getByRole('button', { name: '😀' }).first().click(); @@ -465,14 +468,6 @@ test.describe('Doc Tree', () => { .locator('.--docs--doc-title') .getByRole('button'); await expect(titleEmojiPicker).toHaveText('😀'); - - // Now remove the emoji using the new action - await row.hover(); - await menu.click(); - await page.getByRole('menuitem', { name: 'Remove emoji' }).click(); - - await expect(row.getByText('😀')).toBeHidden(); - await expect(titleEmojiPicker).toBeHidden(); }); test('A child inherit from the parent', async ({ page, browserName }) => { diff --git a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts index e7e21f52b..f8ce62f5e 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts @@ -2,12 +2,7 @@ import path from 'path'; import { Locator, Page, expect, test } from '@playwright/test'; -import { - createDoc, - goToGridDoc, - mockedDocument, - saveContent, -} from './utils-common'; +import { createDoc, mockedDocument, saveContent } from './utils-common'; import { openSuggestionMenu, tryFocusEditorContent, @@ -15,7 +10,7 @@ import { } from './utils-editor'; const openPresenter = async (page: Page) => { - await page.getByLabel('Open the document options').click(); + await page.getByLabel('Open the document options').first().click(); await page.getByRole('menuitem', { name: 'Present' }).click(); const overlay = page.getByRole('dialog', { name: 'Presenter mode' }); @@ -513,9 +508,9 @@ test.describe('Presenter Mode mobile', () => { }, }); - await goToGridDoc(page); + await page.goto('/'); - await page.getByLabel('Open the document options').click(); + await page.getByLabel('Open the document options').first().click(); await expect(page.getByRole('menuitem', { name: 'Present' })).toBeHidden(); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index d0e53848a..66aa639fc 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -446,20 +446,20 @@ export const clickInEditorShareButton = async (page: Page) => { }; export const clickInEditorMenu = async (page: Page, textButton: string) => { - await page - .getByTestId('floating-bar') - .getByRole('button', { name: 'Open the document options' }) - .click(); - await page.getByRole('menuitem', { name: textButton }).click(); + await clickInDocOptionMenu( + page, + page.getByTestId('floating-bar'), + textButton, + ); }; -export const clickInGridMenu = async ( +export const clickInDocOptionMenu = async ( page: Page, - row: Locator, + selector: Locator, textButton: string, ) => { - await row - .getByRole('button', { name: /Open the menu of actions for the document/ }) + await selector + .getByRole('button', { name: /Open the document options/ }) .click(); await page.getByRole('menuitem', { name: textButton }).click(); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx index f355765bb..fb2c4d02c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx @@ -24,7 +24,9 @@ export const DocFloatingBar = () => { {!isDeletedDoc && currentDoc && } - {!isDeletedDoc && currentDoc && } + {!isDeletedDoc && currentDoc && ( + + )} )} diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx index 9f6aab58b..70647aaf4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { APIError, errorCauses, fetchAPI } from '@/api'; +import { syncDocInTree, useTreeContextOrNull } from '@/docs/doc-tree/utils'; import { Doc } from '../types'; @@ -32,16 +33,19 @@ export function useCreateFavoriteDoc({ }: CreateFavoriteDocProps) { const queryClient = useQueryClient(); const { t } = useTranslation(); + const treeContext = useTreeContextOrNull(); return useMutation({ mutationFn: createFavoriteDoc, - onSuccess: () => { + onSuccess: (_data, { id }) => { listInvalidQueries?.forEach((queryKey) => { void queryClient.invalidateQueries({ queryKey: [queryKey], }); }); + syncDocInTree(treeContext, id, { is_favorite: true }); + const message = t('Document starred successfully!'); announce(message, 'polite'); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx index 863803a4f..fc61f7589 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { APIError, errorCauses, fetchAPI } from '@/api'; +import { syncDocInTree, useTreeContextOrNull } from '@/docs/doc-tree/utils'; import { Doc } from '../types'; @@ -32,16 +33,19 @@ export function useDeleteFavoriteDoc({ }: DeleteFavoriteDocProps) { const queryClient = useQueryClient(); const { t } = useTranslation(); + const treeContext = useTreeContextOrNull(); return useMutation({ mutationFn: deleteFavoriteDoc, - onSuccess: () => { + onSuccess: (_data, { id }) => { listInvalidQueries?.forEach((queryKey) => { void queryClient.invalidateQueries({ queryKey: [queryKey], }); }); + syncDocInTree(treeContext, id, { is_favorite: false }); + const message = t('Document unstarred successfully!'); announce(message, 'polite'); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx index 2ad394f3a..cef8bed47 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx @@ -1,14 +1,14 @@ import { Button, + ButtonProps, DropdownMenu, DropdownMenuItem, - useTreeContext, } from '@gouvfr-lasuite/ui-components'; import { Present } from '@gouvfr-lasuite/ui-components/icons'; import { announce } from '@react-aria/live-announcer'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; -import { useEffect, useMemo, useState } from 'react'; +import { memo, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Text } from '@/components/Text'; @@ -17,15 +17,16 @@ import { getWordCount } from '@/docs/doc-editor/utils'; import { printDocumentWithStyles } from '@/docs/doc-export/utils_print'; import { usePresenterStore } from '@/docs/doc-presenter/stores'; import { useDetachDoc } from '@/docs/doc-tree/api/useDetach'; +import { useTreeContextOrNull } from '@/docs/doc-tree/utils'; import { useAuth } from '@/features/auth'; import ContentCopyIcon from '@/icons/copy.svg'; import DocMoveInIcon from '@/icons/doc-move-in.svg'; import DocMoveOutIcon from '@/icons/doc-move-out.svg'; -import DownloadSVG from '@/icons/download.svg'; -import HistorySVG from '@/icons/history.svg'; -import LeaveSVG from '@/icons/leave.svg'; +import DownloadIcon from '@/icons/download.svg'; +import HistoryIcon from '@/icons/history.svg'; +import LeaveIcon from '@/icons/leave.svg'; import LinkIcon from '@/icons/link.svg'; -import MoreSVG from '@/icons/more_horiz.svg'; +import MoreIcon from '@/icons/more_horiz.svg'; import PrintIcon from '@/icons/print.svg'; import SharedIcon from '@/icons/shared.svg'; import StarSlashIcon from '@/icons/star-slash.svg'; @@ -41,7 +42,7 @@ import { useDeleteFavoriteDoc, useDuplicateDoc, } from '../api'; -import { useCopyDocLink } from '../hooks'; +import { useCopyDocLink, useTrans } from '../hooks'; import { Doc, Role } from '../types'; const DocMoveModal = dynamic( @@ -96,13 +97,24 @@ const ModalExport = dynamic( interface DocToolBoxProps { doc: Doc; + isCurrentDoc: boolean; + buttonProps?: ButtonProps; + onOpenChange?: (isOpen: boolean) => void; + optionsDefault?: DropdownMenuItem[]; } -export const DocToolBox = ({ doc }: DocToolBoxProps) => { +const DocToolBoxComponent = ({ + buttonProps, + doc, + isCurrentDoc, + onOpenChange, + optionsDefault, +}: DocToolBoxProps) => { const { t } = useTranslation(); - const treeContext = useTreeContext(); + const { untitledDocument } = useTrans(); + const treeContext = useTreeContextOrNull(); const router = useRouter(); - const isTopParent = doc.id === treeContext?.root?.id; // it can be a child but not for the current user + const isTopParent = !treeContext || doc.id === treeContext?.root?.id; // it can be a child but not for the current user const { authenticated } = useAuth(); const [openDropdown, setOpenDropdown] = useState(false); const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false); @@ -111,17 +123,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false); const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false); const [isModalMoveOpen, setIsModalMoveOpen] = useState(false); + const { onClick: onButtonClick, ...buttonPropsLeft } = buttonProps || {}; - const { editor } = useEditorStore(); + const editor = useEditorStore((state) => state.editor); const wordCountLabel = useMemo(() => { - if (openDropdown) { + if (openDropdown && isCurrentDoc && editor) { return t('Word count: {{count}} words', { count: getWordCount(editor), description: 'In the document options menu, showing the number of words in the document.', }); } - }, [editor, openDropdown, t]); + }, [editor, isCurrentDoc, openDropdown, t]); useEffect(() => { if (wordCountLabel) { @@ -131,8 +144,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const { mutate: detachDoc } = useDetachDoc(); - const { restoreFocus, addLastFocus } = useFocusStore(); - const { isMobile } = useResponsiveStore(); + const restoreFocus = useFocusStore((state) => state.restoreFocus); + const addLastFocus = useFocusStore((state) => state.addLastFocus); + const isMobile = useResponsiveStore((state) => state.isMobile); const copyDocLink = useCopyDocLink(doc.id); const openPresenter = usePresenterStore((state) => state.open); @@ -155,7 +169,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { description: 'Dropdown menu item to copy the document link', }), icon: