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: