diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91a8e0aad..e85e8de6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and this project adheres to
- ♻️(backend) allow global search in sub documents
- ✨(backend) add a breadcrumb in the search response
+- ♻️(frontend) move doc action buttons to fix toolbar #2360
### Fixed
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
index 71fb3dccd..dcc8afc3d 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
@@ -7,6 +7,7 @@ import { PDFParse } from 'pdf-parse';
import {
TestLanguage,
+ clickInEditorMenu,
createDoc,
verifyDocName,
waitForLanguageSwitch,
@@ -24,11 +25,7 @@ test.describe('Doc Export', () => {
browserName,
}) => {
await createDoc(page, 'doc-editor', browserName, 1);
- await page
- .getByRole('button', {
- name: 'Export the document',
- })
- .click();
+ await clickInEditorMenu(page, 'Download');
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
@@ -54,11 +51,7 @@ test.describe('Doc Export', () => {
test('it exports the doc to docx', async ({ page, browserName }) => {
const randomDoc = await overrideDocContent({ page, browserName });
- await page
- .getByRole('button', {
- name: 'Export the document',
- })
- .click();
+ await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
@@ -84,11 +77,7 @@ test.describe('Doc Export', () => {
test('it exports the doc to odt', async ({ page, browserName }) => {
const randomDoc = await overrideDocContent({ page, browserName });
- await page
- .getByRole('button', {
- name: 'Export the document',
- })
- .click();
+ await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Odt' }).click();
@@ -139,11 +128,7 @@ test.describe('Doc Export', () => {
// Give some time for the image to be fully processed
await page.waitForTimeout(1000);
- await page
- .getByRole('button', {
- name: 'Export the document',
- })
- .click();
+ await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'HTML' }).click();
@@ -231,11 +216,7 @@ test.describe('Doc Export', () => {
.fill('https://docs.numerique.gouv.fr/assets/logo-gouv.png');
await page.getByText('Embed image').click();
- await page
- .getByRole('button', {
- name: 'Export the document',
- })
- .click();
+ await clickInEditorMenu(page, 'Download');
await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -322,11 +303,7 @@ test.describe('Doc Export', () => {
await overrideDocContent({ page, browserName });
- await page
- .getByRole('button', {
- name: 'Export the document',
- })
- .click();
+ await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Print' }).click();
@@ -375,11 +352,7 @@ test.describe('Doc Export', () => {
const randomDoc = await overrideDocContent({ page, browserName });
- await page
- .getByRole('button', {
- name: 'Export the document',
- })
- .click();
+ await clickInEditorMenu(page, 'Download');
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
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 20c2bd155..5c5e91a6d 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
@@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
import {
+ clickInEditorMenu,
clickInEditorShareButton,
createDoc,
getGridRow,
@@ -83,15 +84,14 @@ test.describe('Doc Header', () => {
await page.getByRole('button', { name: 'close' }).first().click();
- await expect(card.getByText('Public document')).toBeVisible();
-
+ await expect(card.getByText('Public ·')).toBeVisible();
await expect(card.getByText('Owner ·')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
+ await page
+ .getByRole('button', { name: 'Open the document options' })
+ .click();
await expect(
- page.getByRole('button', { name: 'Export the document' }),
- ).toBeVisible();
- await expect(
- page.getByRole('button', { name: 'Open the document options' }),
+ page.getByRole('menuitem', { name: 'Download' }),
).toBeVisible();
});
@@ -186,17 +186,15 @@ test.describe('Doc Header', () => {
await createDoc(page, 'doc-update-emoji', browserName, 1);
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
- const optionMenu = page.getByLabel('Open the document options');
- const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' });
- const removeEmojiMenuItem = page.getByRole('menuitem', {
- name: 'Remove emoji',
+ const addEmoji = page.getByRole('button', { name: 'Add icon' });
+ const removeEmoji = page.getByRole('button', {
+ name: 'Remove icon',
});
// Top parent should not have emoji picker
await expect(emojiPicker).toBeHidden();
- await optionMenu.click();
- await expect(addEmojiMenuItem).toBeHidden();
- await expect(removeEmojiMenuItem).toBeHidden();
+ await expect(addEmoji).toBeHidden();
+ await expect(removeEmoji).toBeHidden();
await page.keyboard.press('Escape');
const { name: docChild } = await createRootSubPage(
@@ -211,9 +209,8 @@ test.describe('Doc Header', () => {
await expect(emojiPicker).toBeHidden();
// Add emoji
- await optionMenu.click();
- await expect(removeEmojiMenuItem).toBeHidden();
- await addEmojiMenuItem.click();
+ await expect(removeEmoji).toBeHidden();
+ await addEmoji.click();
// The 1 April the emoji is a fish
await expect(emojiPicker).toHaveText(/📄|🐟/);
@@ -235,17 +232,15 @@ test.describe('Doc Header', () => {
await expect(row.getByText('😀')).toBeVisible();
// Remove emoji
- await optionMenu.click();
- await expect(addEmojiMenuItem).toBeHidden();
- await removeEmojiMenuItem.click();
+ await expect(addEmoji).toBeHidden();
+ await removeEmoji.click();
await expect(emojiPicker).toBeHidden();
});
test('it deletes the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
- await page.getByLabel('Open the document options').click();
- await page.getByRole('menuitem', { name: 'Delete document' }).click();
+ await clickInEditorMenu(page, 'Delete');
await expect(
page.getByRole('heading', { name: 'Delete a doc' }),
@@ -301,15 +296,12 @@ test.describe('Doc Header', () => {
page.getByRole('textbox', { name: 'Document title' }),
).toContainText('Mocked document');
- await expect(
- page.getByRole('button', { name: 'Export the document' }),
- ).toBeVisible();
-
await page.getByLabel('Open the document options').click();
await expect(
- page.getByRole('menuitem', { name: 'Delete document' }),
- ).toBeDisabled();
+ page.getByRole('menuitem', { name: 'Download' }),
+ ).toBeVisible();
+ await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeHidden();
// Click somewhere else to close the options
await page.locator('body').click({ position: { x: 0, y: 0 } });
@@ -381,14 +373,12 @@ test.describe('Doc Header', () => {
page.getByRole('textbox', { name: 'Document title' }),
).toContainText('Mocked document');
- await expect(
- page.getByRole('button', { name: 'Export the document' }),
- ).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(
- page.getByRole('menuitem', { name: 'Delete document' }),
- ).toBeDisabled();
+ page.getByRole('menuitem', { name: 'Download' }),
+ ).toBeVisible();
+ await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeHidden();
// Click somewhere else to close the options
await page.locator('body').click({ position: { x: 0, y: 0 } });
@@ -453,14 +443,12 @@ test.describe('Doc Header', () => {
page.getByRole('heading', { name: 'Mocked document' }),
).toBeVisible();
- await expect(
- page.getByRole('button', { name: 'Export the document' }),
- ).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(
- page.getByRole('menuitem', { name: 'Delete document' }),
- ).toBeDisabled();
+ page.getByRole('menuitem', { name: 'Download' }),
+ ).toBeVisible();
+ await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeHidden();
// Click somewhere else to close the options
await page.locator('body').click({ position: { x: 0, y: 0 } });
@@ -707,13 +695,18 @@ test.describe('Documents Header mobile', () => {
await goToGridDoc(page);
- await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden();
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('menuitem', { name: 'Copy link' }),
).toBeVisible();
- await page.getByRole('menuitem', { name: 'Share' }).click();
- await expect(page.getByRole('button', { 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 }) => {
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts
index 7294a52b2..7a698b027 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts
@@ -116,7 +116,7 @@ test.describe('Doc Version', () => {
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('menuitem', { name: 'Version history' }),
- ).toBeDisabled();
+ ).toBeHidden();
});
test('it restores the doc version', async ({ page, browserName }) => {
diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css
index 9c3074223..673c0668c 100644
--- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css
+++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css
@@ -93,7 +93,7 @@
* Tooltip
*/
.c__tooltip {
- padding: var(--c--globals--font--sizes--sm) var(--c--globals--spacings--xxs);
+ padding: var(--c--globals--font--sizes--t) var(--c--globals--spacings--xxs);
}
.c__tooltip .react-aria-OverlayArrow svg {
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx
new file mode 100644
index 000000000..caa82a8da
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx
@@ -0,0 +1,61 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+
+import { AppWrapper } from '@/tests/utils';
+
+const mockUpdateDocEmoji = vi.fn();
+
+vi.mock('@/docs/doc-management', async () => {
+ const actual = await vi.importActual('@/docs/doc-management');
+ return {
+ ...actual,
+ useDocTitleUpdate: () => ({ updateDocEmoji: mockUpdateDocEmoji }),
+ };
+});
+
+import { DocHeader } from '../components/DocHeader';
+
+const doc = {
+ id: 'doc-1',
+ title: 'My document',
+ is_favorite: false,
+ nb_accesses_direct: 1,
+ abilities: {
+ versions_list: true,
+ destroy: true,
+ partial_update: true,
+ duplicate: true,
+ accesses_view: true,
+ },
+} as any;
+
+describe('DocHeader - Add emoji (April Fools easter egg)', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ mockUpdateDocEmoji.mockClear();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ [
+ { emoji: '🐟', date: '2026-04-01' },
+ { emoji: '📄', date: '2026-03-30' },
+ { emoji: '📄', date: '2026-04-02' },
+ ].forEach(({ emoji, date }) => {
+ test(`uses ${emoji} emoji on ${date}`, () => {
+ vi.setSystemTime(new Date(date));
+
+ render(, { wrapper: AppWrapper });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Add icon' }));
+
+ expect(mockUpdateDocEmoji).toHaveBeenCalledWith(
+ 'doc-1',
+ 'My document',
+ emoji,
+ );
+ });
+ });
+});
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxEmoji.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxEmoji.spec.tsx
deleted file mode 100644
index 5e0024cce..000000000
--- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxEmoji.spec.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { render } from '@testing-library/react';
-import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
-
-import { DropdownMenuOption } from '@/components';
-import { AppWrapper } from '@/tests/utils';
-
-const mockUpdateDocEmoji = vi.fn();
-let capturedOptions: DropdownMenuOption[] = [];
-
-vi.mock('@/components', async () => {
- const actual = await vi.importActual('@/components');
- return {
- ...actual,
- DropdownMenu: ({ options }: { options: DropdownMenuOption[] }) => {
- capturedOptions = options;
- return null;
- },
- };
-});
-
-vi.mock('next/router', async () => ({
- ...(await vi.importActual('next/router')),
- useRouter: () => ({ push: vi.fn() }),
-}));
-
-vi.mock('@/docs/doc-management', async () => {
- const actual = await vi.importActual('@/docs/doc-management');
- return {
- ...actual,
- useDocTitleUpdate: () => ({ updateDocEmoji: mockUpdateDocEmoji }),
- useDocUtils: () => ({ isChild: true, isTopRoot: false }),
- useCopyDocLink: () => vi.fn(),
- useCreateFavoriteDoc: () => ({ mutate: vi.fn() }),
- useDeleteFavoriteDoc: () => ({ mutate: vi.fn() }),
- useDuplicateDoc: () => ({ mutate: vi.fn() }),
- };
-});
-
-vi.mock('@/stores', () => ({
- useFocusStore: (selector?: (state: any) => any) => {
- const state = { addLastFocus: vi.fn(), restoreFocus: vi.fn() };
- return selector ? selector(state) : state;
- },
- useResponsiveStore: () => ({
- isSmallMobile: false,
- isMobile: false,
- isDesktop: true,
- }),
-}));
-
-vi.mock('../hooks/useCopyCurrentEditorToClipboard', () => ({
- useCopyCurrentEditorToClipboard: () => vi.fn(),
-}));
-
-import { DocToolBox } from '../components/DocToolBox';
-
-const doc = {
- id: 'doc-1',
- title: 'My document',
- is_favorite: false,
- nb_accesses_direct: 1,
- abilities: {
- versions_list: true,
- destroy: true,
- partial_update: true,
- duplicate: true,
- accesses_view: true,
- },
-} as any;
-
-describe('DocToolBox - Add emoji (April Fools easter egg)', () => {
- beforeEach(() => {
- vi.useFakeTimers();
- mockUpdateDocEmoji.mockClear();
- });
-
- afterEach(() => {
- vi.useRealTimers();
- });
-
- [
- { emoji: '🐟', date: '2026-04-01' },
- { emoji: '📄', date: '2026-03-30' },
- { emoji: '📄', date: '2026-04-02' },
- ].forEach(({ emoji, date }) => {
- test(`uses ${emoji} emoji on ${date}`, () => {
- vi.setSystemTime(new Date(date));
-
- render(, { wrapper: AppWrapper });
-
- const addEmojiOption = capturedOptions.find(
- (o) => o.label === 'Add emoji',
- );
- void addEmojiOption?.callback?.();
-
- expect(mockUpdateDocEmoji).toHaveBeenCalledWith(
- 'doc-1',
- 'My document',
- emoji,
- );
- });
- });
-});
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx
index e0e693d31..087940fa0 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxLicence.spec.tsx
@@ -9,9 +9,33 @@ vi.mock('next/router', async () => ({
...(await vi.importActual('next/router')),
useRouter: () => ({
push: vi.fn(),
+ pathname: '/docs/doc-1',
}),
}));
+vi.mock('@gouvfr-lasuite/ui-kit', async () => {
+ const actual = await vi.importActual('@gouvfr-lasuite/ui-kit');
+ return {
+ ...actual,
+ DropdownMenu: ({ options, children }: any) => (
+ <>
+ {children}
+
+ {options
+ .filter((o: any) => !o.isHidden)
+ .map((o: any) => (
+ - {o.label}
+ ))}
+
+ >
+ ),
+ };
+});
+
+vi.mock('../hooks/useCopyCurrentEditorToClipboard', () => ({
+ useCopyCurrentEditorToClipboard: () => vi.fn(),
+}));
+
const doc = {
nb_accesses: 1,
abilities: {
@@ -39,9 +63,7 @@ describe('DocToolBox - Licence', () => {
wrapper: AppWrapper,
});
- expect(
- await screen.findByLabelText('Export the document'),
- ).toBeInTheDocument();
+ expect(await screen.findByText('Download')).toBeInTheDocument();
}, 15000);
test('The export button is not rendered when MIT version is activated', async () => {
@@ -53,12 +75,6 @@ describe('DocToolBox - Licence', () => {
wrapper: AppWrapper,
});
- expect(
- screen.getByLabelText('Open the document options'),
- ).toBeInTheDocument();
-
- expect(
- screen.queryByLabelText('Export the document'),
- ).not.toBeInTheDocument();
+ expect(screen.queryByText('Download')).not.toBeInTheDocument();
}, 15000);
});
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
index 43c4a7eab..2a71202ed 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
@@ -1,62 +1,90 @@
+import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
+import RemoveEmojiSVG from '@/assets/icons/ui-kit/face-remove.svg';
+import AddEmojiSVG from '@/assets/icons/ui-kit/face.svg';
import { Box, HorizontalSeparator } from '@/components';
-import { useCunninghamTheme } from '@/cunningham';
-import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
-import { useResponsiveStore } from '@/stores';
+import {
+ Doc,
+ getEmojiAndTitle,
+ useDocTitleUpdate,
+ useDocUtils,
+ useIsCollaborativeEditable,
+} from '@/docs/doc-management';
import { AlertNetwork } from './AlertNetwork';
import { AlertRestore } from './AlertRestore';
import { DocHeaderInfo } from './DocHeaderInfo';
import { DocTitle } from './DocTitle';
-import { DocToolBox } from './DocToolBox';
interface DocHeaderProps {
doc: Doc;
}
export const DocHeader = ({ doc }: DocHeaderProps) => {
- const { spacingsTokens } = useCunninghamTheme();
- const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
const { isEditable } = useIsCollaborativeEditable(doc);
const isDeletedDoc = !!doc.deleted_at;
+ // Emoji Management
+ const { emoji } = getEmojiAndTitle(doc.title ?? '');
+ const { updateDocEmoji } = useDocTitleUpdate();
+ const { isTopRoot } = useDocUtils(doc);
+ const displayEmojiButton = doc.abilities.partial_update && !isTopRoot;
return (
<>
- {isDeletedDoc && }
- {!isEditable && }
-
-
-
-
-
-
-
- {!isDeletedDoc && }
-
+ {isDeletedDoc && }
+ {!isEditable && }
-
+
+
+ {displayEmojiButton && (
+
+ )}
+
+
+
+
+
>
);
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx
index 273c15c89..8de22bd4a 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx
@@ -2,7 +2,7 @@ import { t } from 'i18next';
import PublicSVG from '@/assets/icons/ui-kit/public.svg';
import ProtedtedSVG from '@/assets/icons/ui-kit/vpn_lock.svg';
-import { Text } from '@/components';
+import { Box, Text } from '@/components';
import { useConfig } from '@/core';
import {
Doc,
@@ -40,7 +40,7 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
}
return (
- <>
+
{
{dateToDisplay}
- >
+
);
};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
index 7a3ce2ea2..7c87ac542 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
@@ -42,11 +42,7 @@ export const DocTitleText = () => {
return (
-
+
{currentDoc?.title || untitledDocument}
@@ -110,7 +106,7 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
};
const DocTitleInput = ({ doc }: DocTitleProps) => {
- const { isLargeScreen } = useResponsiveStore();
+ const { isSmallMobile } = useResponsiveStore();
const { t } = useTranslation();
const { isTopRoot } = useDocUtils(doc);
const { untitledDocument } = useTrans();
@@ -200,7 +196,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
$minHeight="40px"
>
{!isTopRoot && }
-
{
}
onPasteCapture={handlePaste}
onDropCapture={handleDrop}
- $padding={{ right: 'big' }}
$css={css`
&[contenteditable='true']:empty:not(:focus):before {
content: '${untitledDocument}';
@@ -227,11 +221,12 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
pointer-events: none;
font-style: italic;
}
- font-size: ${isLargeScreen
- ? 'var(--c--globals--font--sizes--h2)'
- : 'var(--c--globals--font--sizes--sm)'};
+ font-size: ${isSmallMobile
+ ? 'var(--c--globals--font--sizes--h4)'
+ : 'var(--c--globals--font--sizes--h2)'};
font-weight: 700;
outline: none;
+ width: 100%;
`}
>
{titleDisplay}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index 7d2f749e0..78380ca3d 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -1,42 +1,36 @@
import { Button, useModal } from '@gouvfr-lasuite/cunningham-react';
-import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
+import {
+ DropdownMenu,
+ DropdownMenuOption,
+ useTreeContext,
+} from '@gouvfr-lasuite/ui-kit';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { css } from 'styled-components';
import AddLinkSVG from '@/assets/icons/ui-kit/add_link.svg';
import ContentCopySVG from '@/assets/icons/ui-kit/content_copy.svg';
import DeleteSVG from '@/assets/icons/ui-kit/delete.svg';
import DownloadSVG from '@/assets/icons/ui-kit/download.svg';
-import RemoveEmojiSVG from '@/assets/icons/ui-kit/face-remove.svg';
-import AddEmojiSVG from '@/assets/icons/ui-kit/face.svg';
+import SharedSVG from '@/assets/icons/ui-kit/group.svg';
import HistorySVG from '@/assets/icons/ui-kit/history.svg';
import KeepSVG from '@/assets/icons/ui-kit/keep.svg';
import KeepOffSVG from '@/assets/icons/ui-kit/keep_off.svg';
import MarkdownCopySVG from '@/assets/icons/ui-kit/markdown_copy.svg';
-import {
- Box,
- DropdownMenu,
- DropdownMenuOption,
- Icon,
- IconOptions,
-} from '@/components';
-import { useCunninghamTheme } from '@/cunningham';
+import MoreSVG from '@/assets/icons/ui-kit/more_horiz.svg';
import {
Doc,
KEY_DOC,
KEY_LIST_DOC,
KEY_LIST_FAVORITE_DOC,
- getEmojiAndTitle,
useCopyDocLink,
useCreateFavoriteDoc,
useDeleteFavoriteDoc,
- useDocTitleUpdate,
useDocUtils,
useDuplicateDoc,
} from '@/docs/doc-management';
+import { useAuth } from '@/features/auth';
import { useFocusStore, useResponsiveStore } from '@/stores';
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
@@ -57,6 +51,14 @@ const ModalSelectVersion = dynamic(
{ ssr: false },
);
+const DocShareModal = dynamic(
+ () =>
+ import('@/docs/doc-share/components/DocShareModal').then((mod) => ({
+ default: mod.DocShareModal,
+ })),
+ { ssr: false },
+);
+
const ModalExport =
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false'
? dynamic(
@@ -76,16 +78,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation();
const treeContext = useTreeContext();
const router = useRouter();
- const { isChild, isTopRoot } = useDocUtils(doc);
-
- const { spacingsTokens, colorsTokens } = useCunninghamTheme();
+ const { isTopRoot } = useDocUtils(doc);
+ const { authenticated } = useAuth();
+ const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
+ const [openDropdown, setOpenDropdown] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
+ const shareModal = useModal();
const selectHistoryModal = useModal();
- const { addLastFocus, restoreFocus } = useFocusStore();
- const { isSmallMobile, isMobile } = useResponsiveStore();
+ const { restoreFocus } = useFocusStore();
+ const { isMobile } = useResponsiveStore();
const copyDocLink = useCopyDocLink(doc.id);
const { mutate: duplicateDoc } = useDuplicateDoc({
onSuccess: (data) => {
@@ -99,19 +103,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC],
});
- // Emoji Management
- const { emoji } = getEmojiAndTitle(doc.title ?? '');
- const { updateDocEmoji } = useDocTitleUpdate();
-
const options: DropdownMenuOption[] = [
- {
- label: t('Export'),
- icon: ,
- callback: () => {
- setIsModalExportOpen(true);
- },
- show: !!ModalExport && isSmallMobile,
- },
{
label: doc.is_favorite ? t('Unpin') : t('Pin'),
icon: doc.is_favorite ? (
@@ -126,55 +118,53 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
makeFavoriteDoc.mutate({ id: doc.id });
}
},
+ isHidden: !doc.abilities.favorite,
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
- },
- {
- label: t('Version history'),
- icon: ,
- disabled: !doc.abilities.versions_list,
- callback: () => {
- selectHistoryModal.open();
- },
- show: !isMobile,
- showSeparator: isTopRoot ? true : false,
- },
- {
- label: t('Remove emoji'),
- icon: ,
- callback: () => {
- updateDocEmoji(doc.id, doc.title ?? '', '');
- },
showSeparator: true,
- show: !!emoji && doc.abilities.partial_update && !isTopRoot,
- },
- {
- label: t('Add emoji'),
- icon: ,
- callback: () => {
- const today = new Date();
- const isAprilFools = today.getMonth() === 3 && today.getDate() === 1;
- updateDocEmoji(doc.id, doc.title ?? '', isAprilFools ? '🐟' : '📄');
- },
- showSeparator: true,
- show: !emoji && doc.abilities.partial_update && !isTopRoot,
},
{
label: t('Copy link'),
icon: ,
callback: copyDocLink,
},
+ {
+ label: t('Share'),
+ icon: ,
+ callback: () => {
+ shareModal.open();
+ },
+ isHidden: !isTopRoot || !authenticated,
+ },
+ {
+ label: t('Download'),
+ icon: ,
+ callback: () => {
+ setIsModalExportOpen(true);
+ },
+ isHidden: !ModalExport,
+ },
{
label: t('Copy as {{format}}', { format: 'Markdown' }),
icon: ,
callback: () => {
void copyCurrentEditorToClipboard('markdown');
},
+ showSeparator: isMobile || !doc.abilities.versions_list,
+ },
+ {
+ label: t('Version history'),
+ icon: ,
+ isDisabled: !doc.abilities.versions_list,
+ callback: () => {
+ selectHistoryModal.open();
+ },
+ isHidden: isMobile || !doc.abilities.versions_list,
showSeparator: true,
},
{
label: t('Duplicate'),
icon: ,
- disabled: !doc.abilities.duplicate,
+ isDisabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
docId: doc.id,
@@ -182,65 +172,40 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
canSave: doc.abilities.partial_update,
});
},
+ isHidden: !doc.abilities.duplicate,
showSeparator: true,
},
{
- label: isChild ? t('Delete sub-document') : t('Delete document'),
+ label: t('Delete'),
icon: ,
- disabled: !doc.abilities.destroy,
callback: () => {
setIsModalRemoveOpen(true);
},
+ isHidden: !doc.abilities.destroy,
},
];
- const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
-
return (
-
-
+ true}
+ onOpenChange={setOpenDropdown}
>
- {!isSmallMobile && ModalExport && (
-
- }
- onClick={(e) => {
- addLastFocus(e.currentTarget);
- setIsModalExportOpen(true);
- }}
- size={isSmallMobile ? 'small' : 'medium'}
- aria-label={t('Export the document')}
- />
- )}
-
-
-
-
+ }
+ color="neutral"
+ variant="tertiary"
+ onClick={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ setOpenDropdown((o) => !o);
+ }}
+ />
+
{isModalExportOpen && ModalExport && (
{
doc={doc}
/>
)}
-
+ {shareModal.isOpen && (
+ {
+ shareModal.close();
+ restoreFocus();
+ }}
+ doc={doc}
+ isRootDoc={treeContext?.root?.id === doc.id}
+ />
+ )}
+ >
);
};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx
index 6c0c9d512..48e5ae941 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { css } from 'styled-components';
-import { Box } from '@/components';
+import { Box, Card } from '@/components';
import { useCunninghamTheme } from '@/cunningham/useCunninghamTheme';
import { useDocStore } from '@/docs/doc-management/stores/useDocStore';
import { DocShareButton } from '@/features/docs/doc-share/components/DocShareButton';
@@ -9,6 +9,8 @@ import { LeftPanelCollapseButton } from '@/features/left-panel/components/LeftPa
import { RightPanelCollapseButton } from '@/features/right-panel/components/RightPanelCollapseButton';
import { useResponsiveStore } from '@/stores';
+import { DocToolBox } from './DocToolBox';
+
/**
* Sticky bar trick (desktop):
* - MainContent has padding `base`; we extend the bar width and apply
@@ -23,7 +25,6 @@ export const FloatingBar = () => {
const isDeletedDoc = !!currentDoc?.deleted_at;
const FLOATING_STYLES = useMemo(() => {
- const base = spacingsTokens['base'];
const sm = spacingsTokens['sm'];
return css`
position: sticky;
@@ -33,7 +34,6 @@ export const FloatingBar = () => {
width: 100%;
min-height: 64px;
padding: ${sm};
- margin-top: calc(-${base});
z-index: 21; // Under editor select box but above other elements (e.g., doc title, suggestion menu)
align-items: flex-start;
isolation: isolate;
@@ -76,7 +76,20 @@ export const FloatingBar = () => {
{isLargeScreen ? : }
{!isDeletedDoc && currentDoc && }
-
+
+
+ {!isDeletedDoc && currentDoc && }
+
);
diff --git a/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx b/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx
index 0dae1c510..ea3ea235c 100644
--- a/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx
+++ b/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx
@@ -1,7 +1,5 @@
import { useEffect, useState } from 'react';
-import { css } from 'styled-components';
-import { Card } from '@/components';
import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
import { useEditorStore } from '@/features/docs/doc-editor/stores/useEditorStore';
import { useHeadingStore } from '@/features/docs/doc-editor/stores/useHeadingStore';
@@ -30,19 +28,9 @@ export const RightPanelCollapseButton = () => {
}
return (
-
+ <>
{hasHeadings && }
{hasThreads && }
-
+ >
);
};
diff --git a/src/frontend/apps/impress/src/features/skeletons/components/DocEditorSkeleton.tsx b/src/frontend/apps/impress/src/features/skeletons/components/DocEditorSkeleton.tsx
index 9ae54d46b..13db619d8 100644
--- a/src/frontend/apps/impress/src/features/skeletons/components/DocEditorSkeleton.tsx
+++ b/src/frontend/apps/impress/src/features/skeletons/components/DocEditorSkeleton.tsx
@@ -1,10 +1,9 @@
import { css, keyframes } from 'styled-components';
import { Box, BoxType } from '@/components';
-import { useCunninghamTheme } from '@/cunningham';
import { useResponsiveStore } from '@/stores';
-import { SkeletonCircle, SkeletonLine } from './SkeletionUI';
+import { SkeletonLine } from './SkeletionUI';
export const DocEditorSkeleton = () => {
return (
@@ -16,6 +15,7 @@ export const DocEditorSkeleton = () => {
$height="100%"
className="--docs--doc-editor-skeleton"
>
+
@@ -23,58 +23,63 @@ export const DocEditorSkeleton = () => {
);
};
+const SkeletonEditorFloatingBar = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
const SkeletonEditorHeader = () => {
const { isDesktop } = useResponsiveStore();
- const { spacingsTokens } = useCunninghamTheme();
return (
-
- {/* Title and metadata skeleton */}
-
- {/* Title - "Untitled Document" style */}
-
+ {/* Title and metadata skeleton */}
+
+ {/* Title - "Untitled Document" style */}
+
- {/* Metadata (role and last update) */}
-
-
-
-
-
- {/* Toolbox skeleton (buttons) */}
-
- {/* Download icon */}
-
- {/* Menu icon */}
-
+ {/* Metadata (role and last update) */}
+
+
-
- {/* Separator */}
-
+
+ {/* Separator */}
+
);
};
diff --git a/src/frontend/apps/impress/src/tests/__mocks__/svgMock.tsx b/src/frontend/apps/impress/src/tests/__mocks__/svgMock.tsx
new file mode 100644
index 000000000..82ea8184b
--- /dev/null
+++ b/src/frontend/apps/impress/src/tests/__mocks__/svgMock.tsx
@@ -0,0 +1,2 @@
+const SvgMock = () => null;
+export default SvgMock;
diff --git a/src/frontend/apps/impress/vitest.config.ts b/src/frontend/apps/impress/vitest.config.ts
index 9a06f4fa4..ed8f3925c 100644
--- a/src/frontend/apps/impress/vitest.config.ts
+++ b/src/frontend/apps/impress/vitest.config.ts
@@ -1,4 +1,6 @@
///
+import path from 'path';
+
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
@@ -16,6 +18,12 @@ export default defineConfig({
'process.env.NODE_ENV': 'test',
},
resolve: {
+ alias: [
+ {
+ find: /^.*\.svg$/,
+ replacement: path.resolve(__dirname, 'src/tests/__mocks__/svgMock.tsx'),
+ },
+ ],
tsconfigPaths: true,
},
});