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: ,
- callback: copyDocLink,
+ callback: () => {
+ copyDocLink();
+ restoreFocus();
+ },
},
{
label: t('Share', {
@@ -166,8 +183,8 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
setIsModalShareOpen(true);
},
isHidden: !authenticated,
+ showSeparator: isCurrentDoc,
},
- { type: 'separator' },
{
label: t('Present', {
description:
@@ -177,17 +194,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
callback: () => {
openPresenter(0);
},
- isHidden: Boolean(doc.deleted_at) || isMobile,
+ isHidden: Boolean(doc.deleted_at) || isMobile || !isCurrentDoc,
testId: `docs-actions-present-${doc.id}`,
},
{
label: t('Download', {
description: 'Dropdown menu item to download the document',
}),
- icon: ,
+ icon: ,
callback: () => {
setIsModalExportOpen(true);
},
+ isHidden: !isCurrentDoc,
},
{
label: t('Print', {
@@ -197,6 +215,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
callback: () => {
printDocumentWithStyles();
},
+ isHidden: !isCurrentDoc,
},
{ type: 'separator' },
{
@@ -212,6 +231,8 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
} else {
makeFavoriteDoc.mutate({ id: doc.id });
}
+
+ restoreFocus();
},
isHidden: !doc.abilities.favorite,
testId: `docs-actions-${doc.is_favorite ? 'unstar' : 'star'}-${doc.id}`,
@@ -270,19 +291,19 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
label: t('History', {
description: 'Dropdown menu item to view the document history',
}),
- icon: ,
+ icon: ,
isDisabled: !doc.abilities.versions_list,
callback: () => {
setIsModalHistoryOpen(true);
},
- isHidden: isMobile || !doc.abilities.versions_list,
+ isHidden: isMobile || !doc.abilities.versions_list || !isCurrentDoc,
showSeparator: true,
},
{
label: t('Leave', {
description: 'Dropdown menu item to leave the document',
}),
- icon: ,
+ icon: ,
callback: () => {
setIsModalLeaveOpen(true);
},
@@ -302,35 +323,44 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
setIsModalRemoveOpen(true);
},
isHidden: !doc.abilities.destroy,
- },
- {
- type: 'separator',
+ showSeparator: isCurrentDoc,
},
];
return (
<>
true}
- onOpenChange={setOpenDropdown}
+ onOpenChange={(isOpen) => {
+ setOpenDropdown(isOpen);
+ onOpenChange?.(isOpen);
+ }}
bottomMessage={
wordCountLabel && {wordCountLabel}
}
>
}
+ icon={}
color="neutral"
variant="tertiary"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
- setOpenDropdown((o) => !o);
+ const isOpen = !openDropdown;
+ setOpenDropdown(isOpen);
+ onOpenChange?.(isOpen);
addLastFocus(e.currentTarget);
+ onButtonClick?.(
+ e as React.MouseEvent,
+ );
}}
+ {...buttonPropsLeft}
/>
@@ -409,3 +439,5 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
>
);
};
+
+export const DocToolBox = memo(DocToolBoxComponent);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts
index 3e012bf52..7c48b35bb 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts
@@ -5,7 +5,7 @@ import {
} from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
-import { KEY_LIST_DOC } from '@/docs/doc-management/api';
+import { KEY_LIST_DOC, KEY_LIST_FAVORITE_DOC } from '@/docs/doc-management/api';
interface LeaveDocProps {
docId: string;
@@ -36,6 +36,9 @@ export const useLeaveDoc = (options?: UseLeaveDocOptions) => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_DOC],
});
+ void queryClient.invalidateQueries({
+ queryKey: [KEY_LIST_FAVORITE_DOC],
+ });
if (options?.onSuccess) {
void options.onSuccess(data, variables, onMutateResult, context);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx
index 5a950d0e8..7b92a0519 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx
@@ -6,6 +6,7 @@ import {
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc, LinkReach, LinkRole } from '@/docs/doc-management';
+import { syncDocInTree, useTreeContextOrNull } from '@/docs/doc-tree/utils';
export type UpdateDocLinkParams = Pick &
Partial>;
@@ -43,6 +44,7 @@ type UseUpdateDocLinkOptions = UseMutationOptions<
export function useUpdateDocLink(options?: UseUpdateDocLinkOptions) {
const queryClient = useQueryClient();
+ const treeContext = useTreeContextOrNull();
return useMutation({
mutationFn: updateDocLink,
@@ -54,6 +56,17 @@ export function useUpdateDocLink(options?: UseUpdateDocLinkOptions) {
});
});
+ /**
+ * Can be loaded from the doc tree, we need to resync the doc tree
+ * in order to reflect the changes in the tree view.
+ */
+ syncDocInTree(treeContext, variables.id, {
+ link_reach: data.link_reach,
+ link_role: data.link_role,
+ computed_link_reach: data.link_reach,
+ computed_link_role: data.link_role,
+ });
+
options?.onSuccess?.(data, variables, onMutateResult, context);
},
});
diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
index fd77be9b8..e55cfbf55 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
@@ -1,157 +1,39 @@
-import {
- Button,
- ButtonElement,
- DropdownMenu,
- DropdownMenuOption,
- useArrowRoving,
- useModal,
- useTreeContext,
-} from '@gouvfr-lasuite/ui-components';
+import { Button, ButtonProps } from '@gouvfr-lasuite/ui-components';
import { useRouter } from 'next/router';
-import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import AddSVG from '@/assets/icons/ui-kit/add.svg';
-import MoreHorizSVG from '@/assets/icons/ui-kit/more_horiz.svg';
import { Box, Icon } from '@/components';
-import {
- Doc,
- ModalRemoveDoc,
- Role,
- getEmojiAndTitle,
- useCopyDocLink,
- useCreateChildDoc,
- useDocTitleUpdate,
-} from '@/docs/doc-management';
-import { useDuplicateDoc } from '@/docs/doc-management/api';
-import DocMoveOutIcon from '@/icons/doc-move-out.svg';
+import { Doc, useCreateChildDoc, useTrans } from '@/docs/doc-management';
+import { DocToolBox } from '@/docs/doc-management/components/DocToolBox';
+import MoreIcon from '@/icons/more_horiz.svg';
-import { useDetachDoc } from '../api/useDetach';
+import { CLASS_TREE_ITEM_ACTIONS } from '../utils';
+
+// Module-level so the reference stays stable and the memoized `DocToolBox`
+// isn't forced to re-render on every parent render.
+const TOOLBOX_BUTTON_PROPS: ButtonProps = {
+ color: 'brand',
+ icon: ,
+ size: 'nano',
+ tabIndex: -1,
+};
type DocTreeItemActionsProps = {
doc: Doc;
- isOpen?: boolean;
- isRoot?: boolean;
onCreateSuccess?: (newDoc: Doc) => void;
onOpenChange?: (isOpen: boolean) => void;
- parentId?: string | null;
- actionsRef?: React.RefObject;
- buttonOptionRef?: React.RefObject;
};
export const DocTreeItemActions = ({
doc,
- isOpen,
- isRoot = false,
onCreateSuccess,
onOpenChange,
- parentId,
- actionsRef,
- buttonOptionRef,
}: DocTreeItemActionsProps) => {
- const internalActionsRef = useRef(null);
- const targetActionsRef = actionsRef ?? internalActionsRef;
- const internalButtonRef = useRef(null);
- const targetButtonRef = buttonOptionRef ?? internalButtonRef;
const router = useRouter();
const { t } = useTranslation();
- const deleteModal = useModal();
- const copyLink = useCopyDocLink(doc.id);
- const { mutate: detachDoc } = useDetachDoc();
- const treeContext = useTreeContext();
-
- // Keyboard navigation inside the actions toolbar (ArrowLeft / ArrowRight).
- useArrowRoving(targetActionsRef.current);
-
- const { mutate: duplicateDoc } = useDuplicateDoc({
- onSuccess: (duplicatedDoc) => {
- // Reset the tree context root will reset the full tree view.
- treeContext?.setRoot(null);
- void router.push(`/docs/${duplicatedDoc.id}`);
- },
- });
-
- // Emoji Management
- const { emoji } = getEmojiAndTitle(doc.title ?? '');
- const { updateDocEmoji } = useDocTitleUpdate();
- const removeEmoji = () => {
- updateDocEmoji(doc.id, doc.title ?? '', '');
- };
-
- const handleDetachDoc = () => {
- if (!treeContext?.root) {
- return;
- }
-
- detachDoc(
- { documentId: doc.id, rootId: treeContext.root.id },
- {
- onSuccess: () => {
- if (treeContext.root) {
- treeContext.treeData.setSelectedNode(treeContext.root);
- void router.push(`/docs/${treeContext.root.id}`).then(() => {
- setTimeout(() => {
- treeContext?.treeData.deleteNode(doc.id);
- }, 100);
- });
- }
- },
- },
- );
- };
-
- const options: DropdownMenuOption[] = [
- {
- label: t('Copy link'),
- icon: ,
- callback: copyLink,
- },
- ...(!isRoot
- ? [
- ...(emoji && doc.abilities.partial_update
- ? [
- {
- label: t('Remove emoji'),
- icon: ,
- callback: removeEmoji,
- },
- ]
- : []),
- {
- label: t('Move to my docs'),
- isDisabled: doc.user_role !== Role.OWNER,
- icon: (
-
- ),
- callback: handleDetachDoc,
- },
- ]
- : []),
- {
- label: t('Duplicate'),
- icon: ,
- isDisabled: !doc.abilities.duplicate,
- callback: () => {
- duplicateDoc({
- docId: doc.id,
- with_accesses: false,
- canSave: doc.abilities.partial_update,
- });
- },
- },
- {
- label: t('Delete'),
- isDisabled: !doc.abilities.destroy,
- icon: ,
- callback: deleteModal.open,
- },
- ];
+ const { untitledDocument } = useTrans();
const { mutate: createChildDoc } = useCreateChildDoc({
onSuccess: (newDoc) => {
@@ -160,97 +42,56 @@ export const DocTreeItemActions = ({
},
});
- const onSuccessDelete = () => {
- const isTopParent = doc.id === treeContext?.root?.id && !parentId;
- const parentIdComputed = parentId || treeContext?.root?.id;
-
- if (isTopParent) {
- void router.push(`/`);
- } else if (parentIdComputed) {
- void router.push(`/docs/${parentIdComputed}`).then(() => {
- setTimeout(() => {
- treeContext?.treeData.deleteNode(doc.id);
- }, 100);
- });
- }
- };
-
return (
-
-
-
-
-
- {doc.abilities.children_create && (
-
- {deleteModal.isOpen && (
-
+ createChildDoc({ parentId: doc.id });
+ }}
+ aria-label={t('Add a sub page')}
+ data-testid="doc-tree-item-actions-add-child"
+ tabIndex={-1}
+ color="brand"
+ variant="tertiary"
+ size="small"
+ >
+ }
+ />
+
)}
);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts
index 952ae9e06..36e377275 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts
@@ -1,8 +1,11 @@
import {
+ TreeContext,
+ TreeContextType,
TreeDataItem,
TreeViewDataType,
TreeViewNodeTypeEnum,
} from '@gouvfr-lasuite/ui-components';
+import { useContext } from 'react';
import { Doc } from '../doc-management';
@@ -24,6 +27,35 @@ export const subPageToTree = (children: Doc[]): TreeViewDataType[] => {
return children;
};
+export const useTreeContextOrNull = (): TreeContextType | null =>
+ useContext(TreeContext);
+
+/**
+ * The doc tree keeps its own copy of each doc (`treeContext.treeData` /
+ * `treeContext.root`), which react-query cache invalidations don't reach. Code
+ * mutating a doc from a tree item must sync the changed fields into the tree by
+ * hand, otherwise the tree UI stays stale until it is reloaded.
+ *
+ * No-op when there is no tree context (e.g. the doc grid or the doc header),
+ * where the react-query cache is the single source of truth.
+ */
+export const syncDocInTree = (
+ treeContext: TreeContextType | null,
+ docId: string,
+ data: Partial,
+) => {
+ if (!treeContext) {
+ return;
+ }
+
+ const { root } = treeContext;
+ if (root && root.id === docId) {
+ treeContext.setRoot({ ...root, ...data });
+ } else if (treeContext.treeData.getNode(docId)) {
+ treeContext.treeData.updateNode(docId, data);
+ }
+};
+
export const findIndexInTree = (
nodes: TreeDataItem>[],
key: string,
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
index 9180c412c..24c2d180d 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
@@ -1,12 +1,9 @@
import {
- Button,
- DropdownMenu,
+ ButtonProps,
DropdownMenuItem,
VariantType,
useToastProvider,
} from '@gouvfr-lasuite/ui-components';
-import dynamic from 'next/dynamic';
-import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon } from '@/components/Icon';
@@ -14,64 +11,23 @@ import {
type Doc,
KEY_LIST_DOC,
KEY_LIST_FAVORITE_DOC,
- useCreateFavoriteDoc,
- useDeleteFavoriteDoc,
- useDuplicateDoc,
useRestoreDoc,
- useTrans,
} from '@/docs/doc-management';
-import ContentCopyIcon from '@/icons/content_copy.svg';
-import DeleteIcon from '@/icons/delete.svg';
-import DocMoveInIcon from '@/icons/doc-move-in.svg';
-import GroupIcon from '@/icons/group.svg';
-import LeaveIcon from '@/icons/leave.svg';
+import { DocToolBox } from '@/docs/doc-management/components/DocToolBox';
import MoreIcon from '@/icons/more_horiz.svg';
-import StarSlashIcon from '@/icons/star-slash.svg';
-import StarIcon from '@/icons/star.svg';
-import { focusMainContentStart } from '@/layouts/utils';
-import { useFocusStore } from '@/stores';
import { KEY_LIST_DOC_TRASHBIN } from '../api';
-const DocMoveModal = dynamic(
- () =>
- import('@/docs/doc-management/components/DocMoveModal').then((mod) => ({
- default: mod.DocMoveModal,
- })),
- { ssr: false },
-);
-
-const DocShareModal = dynamic(
- () =>
- import('@/docs/doc-share/components/DocShareModal').then((mod) => ({
- default: mod.DocShareModal,
- })),
- { ssr: false },
-);
-
-const ModalRemoveDoc = dynamic(
- () =>
- import('@/docs/doc-management/components/ModalRemoveDoc').then((mod) => ({
- default: mod.ModalRemoveDoc,
- })),
- { ssr: false },
-);
-
-const ConfirmationLeaveModal = dynamic(
- () =>
- import('@/docs/doc-share/components/ConfirmationLeaveModal').then(
- (mod) => ({
- default: mod.ConfirmationLeaveModal,
- }),
- ),
- { ssr: false },
-);
-
interface DocsGridActionsProps {
doc: Doc;
isInTrashbin?: boolean;
}
+const TOOLBOX_BUTTON_PROPS: ButtonProps = {
+ icon: ,
+ size: 'nano',
+};
+
export const DocsGridActions = ({
doc,
isInTrashbin,
@@ -79,141 +35,11 @@ export const DocsGridActions = ({
return isInTrashbin ? (
) : (
-
- );
-};
-
-const DocsGridActionsGlobal = ({ doc }: { doc: Doc }) => {
- const { t } = useTranslation();
- const { restoreFocus } = useFocusStore();
- const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
- const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
- const [isModalShareOpen, setIsModalShareOpen] = useState(false);
- const [isModalMoveOpen, setIsModalMoveOpen] = useState(false);
-
- const { mutate: duplicateDoc } = useDuplicateDoc({
- onSuccess: () => {
- requestAnimationFrame(() => {
- focusMainContentStart({ preventScroll: true });
- });
- },
- });
-
- const removeFavoriteDoc = useDeleteFavoriteDoc({
- listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_FAVORITE_DOC],
- });
- const makeFavoriteDoc = useCreateFavoriteDoc({
- listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_FAVORITE_DOC],
- });
-
- const options: DropdownMenuItem[] = [
- {
- label: doc.is_favorite ? t('Unstar') : t('Star'),
- icon: doc.is_favorite ? (
-
- ) : (
-
- ),
- callback: () => {
- if (doc.is_favorite) {
- removeFavoriteDoc.mutate({ id: doc.id });
- } else {
- makeFavoriteDoc.mutate({ id: doc.id });
- }
- },
- testId: `docs-grid-actions-${doc.is_favorite ? 'unstar' : 'star'}-${doc.id}`,
- showSeparator: true,
- },
- {
- label: t('Share'),
- icon: ,
- callback: () => {
- setIsModalShareOpen(true);
- },
-
- testId: `docs-grid-actions-share-${doc.id}`,
- },
- {
- label: t('Move into a doc'),
- icon: ,
- callback: () => {
- setIsModalMoveOpen(true);
- },
- testId: `docs-grid-actions-move-${doc.id}`,
- isHidden: !doc.abilities.move,
- },
- {
- label: t('Duplicate'),
- icon: ,
- isDisabled: !doc.abilities.duplicate,
- callback: () => {
- duplicateDoc({
- docId: doc.id,
- with_accesses: false,
- canSave: false,
- });
- },
- showSeparator: true,
- },
- {
- label: t('Leave'),
- icon: ,
- callback: () => {
- setIsModalLeaveOpen(true);
- },
- },
- {
- label: t('Delete'),
- icon: ,
- callback: () => {
- setIsModalRemoveOpen(true);
- },
- isHidden: !doc.abilities.destroy,
- testId: `docs-grid-actions-remove-${doc.id}`,
- },
- ];
-
- return (
- <>
-
- {isModalRemoveOpen && (
- {
- setIsModalRemoveOpen(false);
- restoreFocus();
- }}
- doc={doc}
- />
- )}
- {isModalShareOpen && (
- {
- setIsModalShareOpen(false);
- restoreFocus();
- }}
- />
- )}
- {isModalLeaveOpen && (
- {
- setIsModalLeaveOpen(false);
- restoreFocus();
- }}
- doc={doc}
- />
- )}
- {isModalMoveOpen && (
- {
- setIsModalMoveOpen(false);
- restoreFocus();
- }}
- isOpen={isModalMoveOpen}
- />
- )}
- >
+
);
};
@@ -276,43 +102,12 @@ export const DocsGridTrashbinActions = ({
},
];
- return ;
-};
-
-interface DocsGridDropdownProps {
- doc: Doc;
- options: DropdownMenuItem[];
-}
-
-const DocsGridDropdown = ({ doc, options }: DocsGridDropdownProps) => {
- const { t } = useTranslation();
- const [openDropdown, setOpenDropdown] = useState(false);
- const { addLastFocus } = useFocusStore();
- const { untitledDocument } = useTrans();
-
return (
- true}
- onOpenChange={setOpenDropdown}
- >
- }
- color="neutral"
- variant="tertiary"
- onClick={(e) => {
- e.stopPropagation();
- e.preventDefault();
- setOpenDropdown((o) => !o);
- addLastFocus(e.currentTarget);
- }}
- />
-
+
);
};
diff --git a/src/frontend/apps/impress/src/i18n/translations.json b/src/frontend/apps/impress/src/i18n/translations.json
index f3613cb6c..56040ce11 100644
--- a/src/frontend/apps/impress/src/i18n/translations.json
+++ b/src/frontend/apps/impress/src/i18n/translations.json
@@ -1523,6 +1523,7 @@
"Open new document options": "Ouvrir les options de nouveau document",
"Open root document": "Ouvrir le document racine",
"Open the document options": "Ouvrir les options du document",
+ "Open the document options: {{title}}": "Ouvrir les options du document: {{title}}",
"Open the menu of actions for the document: {{title}}": "Ouvrir le menu des actions du document : {{title}}",
"Open the sharing settings for the document": "Ouvrir les paramètres de partage pour le document",
"Organize": "Organiser",