diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a8e0aad3..e85e8de6e9 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/env.d/development/common.e2e b/env.d/development/common.e2e index 73fb517dde..6a2131c78b 100644 --- a/env.d/development/common.e2e +++ b/env.d/development/common.e2e @@ -6,4 +6,5 @@ Y_PROVIDER_API_BASE_URL=http://y-provider-converter:4444/api/ # Throttle API_DOCUMENT_THROTTLE_RATE=1000/min +API_DOCUMENT_ACCESS_THROTTLE_RATE=1000/min API_CONFIG_THROTTLE_RATE=1000/min 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 ba2945eae5..dcc8afc3db 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)); @@ -288,14 +269,9 @@ test.describe('Doc Export', () => { }); await page - .getByRole('button', { - name: 'Exporter le document', - }) + .getByRole('button', { name: 'Ouvrir les options du document' }) .click(); - - await expect( - page.getByTestId('doc-open-modal-download-button'), - ).toBeVisible(); + await page.getByRole('menuitem', { name: 'Télécharger' }).click(); const downloadPromise = page.waitForEvent('download', (download) => { return download.suggestedFilename().includes(`${randomDocFrench}.pdf`); @@ -327,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(); @@ -380,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-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index 9b900e0f2f..02605f7bd7 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,11 @@ import { expect, test } from '@playwright/test'; -import { createDoc, getGridRow, verifyDocName } from './utils-common'; +import { + clickInEditorShareButton, + createDoc, + getGridRow, + verifyDocName, +} from './utils-common'; import { addNewMember, connectOtherUserToDoc } from './utils-share'; type SmallDoc = { @@ -212,9 +217,7 @@ test.describe('Documents filters', () => { test('it checks the left panel filters', async ({ page, browserName }) => { void page.goto('/'); - // Create my doc const [docName] = await createDoc(page, 'my-doc', browserName, 1); - await verifyDocName(page, docName); // Another user create a doc and share it with me const { cleanup, otherPage, otherBrowserName } = @@ -230,9 +233,7 @@ test.describe('Documents filters', () => { 1, ); - await verifyDocName(otherPage, docShareName); - - await otherPage.getByRole('button', { name: 'Share' }).click(); + await clickInEditorShareButton(otherPage); await addNewMember(otherPage, 0, 'Editor', browserName); 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 5e812dad79..5c5e91a6d0 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,8 @@ import { expect, test } from '@playwright/test'; import { + clickInEditorMenu, + clickInEditorShareButton, createDoc, getGridRow, goToGridDoc, @@ -82,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(); }); @@ -185,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( @@ -210,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(/📄|🐟/); @@ -234,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' }), @@ -300,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 } }); @@ -380,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 } }); @@ -452,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 } }); @@ -706,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 }) => { @@ -733,8 +727,7 @@ test.describe('Documents Header mobile', () => { await goToGridDoc(page); - await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Share' }).click(); + await clickInEditorShareButton(page); const shareModal = page.getByRole('dialog', { name: 'Share the document', diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index 23eabde581..c6c804e8cb 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -1,6 +1,12 @@ import { expect, test } from '@playwright/test'; -import { BROWSERS, createDoc, randomName, verifyDocName } from './utils-common'; +import { + BROWSERS, + clickInEditorShareButton, + createDoc, + randomName, + verifyDocName, +} from './utils-common'; import { writeInEditor } from './utils-editor'; import { connectOtherUserToDoc, updateRoleUser } from './utils-share'; import { SignIn } from './utils-signin'; @@ -271,8 +277,6 @@ test.describe('Document create member', () => { 1, ); - await verifyDocName(page, docTitle); - await writeInEditor({ page, text: 'Hello World' }); const docUrl = page.url(); @@ -294,8 +298,7 @@ test.describe('Document create member', () => { ).toBeVisible(); // First user approves the request - await page.getByRole('button', { name: 'Share' }).click(); - + await clickInEditorShareButton(page); await expect(page.getByText('Access Requests')).toBeVisible(); await expect( page.getByText( @@ -330,7 +333,9 @@ test.describe('Document create member', () => { await updateRoleUser(page, 'Remove access', emailRequest); await expect( otherPage.getByText('Insufficient access rights to view the document.'), - ).toBeVisible(); + ).toBeVisible({ + timeout: 10000, + }); // Cleanup: other user logout await cleanup(); 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 efe7b2100c..abf7e26736 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 @@ -1,6 +1,10 @@ import { expect, test } from '@playwright/test'; -import { createDoc, verifyDocName } from './utils-common'; +import { + clickInEditorShareButton, + createDoc, + verifyDocName, +} from './utils-common'; import { addNewMember } from './utils-share'; test.beforeEach(async ({ page }) => { @@ -114,14 +118,8 @@ test.describe('Document list members', () => { }, ); - const [docTitle] = await createDoc( - page, - 'members-big-invitation-list', - browserName, - 1, - ); - await verifyDocName(page, docTitle); - await page.getByRole('button', { name: 'Share' }).click(); + await createDoc(page, 'members-big-invitation-list', browserName, 1); + await clickInEditorShareButton(page); const prefix = 'doc-share-invitation'; const elements = page.locator(`[data-testid^="${prefix}"]`); @@ -142,11 +140,10 @@ test.describe('Document list members', () => { }); test('it checks the role rules', async ({ page, browserName }) => { - const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1); + await createDoc(page, 'Doc role rules', browserName, 1); - await verifyDocName(page, docTitle); + await clickInEditorShareButton(page); - await page.getByRole('button', { name: 'Share' }).click(); const list = page.getByTestId('doc-share-quick-search'); await expect(list).toBeVisible(); const emailRequest = @@ -208,11 +205,9 @@ test.describe('Document list members', () => { }); test('it checks the delete members', async ({ page, browserName }) => { - const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1); + await createDoc(page, 'Doc role rules', browserName, 1); - await verifyDocName(page, docTitle); - - await page.getByRole('button', { name: 'Share' }).click(); + await clickInEditorShareButton(page); const list = page.getByTestId('doc-share-quick-search'); @@ -227,7 +222,7 @@ test.describe('Document list members', () => { ); await page.getByRole('button', { name: 'close' }).first().click(); - await page.getByRole('button', { name: 'Share' }).first().click(); + await clickInEditorShareButton(page); const userReaderEmail = await addNewMember(page, 0, 'Reader'); 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 9e60c1bc09..456df240ce 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 @@ -127,7 +127,7 @@ test.describe('Doc Trashbin', () => { await navigateToPageFromTree({ page, title: subDocName }); await verifyDocName(page, subDocName); - await clickInEditorMenu(page, 'Delete sub-document'); + await clickInEditorMenu(page, 'Delete'); await page.getByRole('button', { name: 'Delete document' }).click(); await verifyDocName(page, topParent); @@ -152,7 +152,7 @@ test.describe('Doc Trashbin', () => { ).toBeVisible(); await expect(page.getByLabel('Alert deleted document')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled(); + await expect(page.getByRole('button', { name: 'Share' })).toBeHidden(); await expect(page.locator('.bn-editor')).toHaveAttribute( 'contenteditable', 'false', 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 7294a52b2d..7a698b0271 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/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index a049967419..a057b51400 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -220,11 +220,7 @@ test.describe('Doc Visibility: Public', () => { 'It is the card information about the document.', ); - await expect(cardContainer.getByTestId('public-icon')).toBeVisible(); - - await expect( - cardContainer.getByText('Public document', { exact: true }), - ).toBeVisible(); + await expect(cardContainer.getByText('Public ·')).toBeVisible(); await expect(page.getByTestId('search-docs-button')).toBeVisible(); await expect(page.getByTestId('new-doc-button')).toBeVisible(); @@ -240,9 +236,6 @@ test.describe('Doc Visibility: Public', () => { await expect(otherPage.locator('h2').getByText(docTitle)).toBeVisible(); await expect(otherPage.getByTestId('search-docs-button')).toBeHidden(); await expect(otherPage.getByTestId('new-doc-button')).toBeHidden(); - await expect( - otherPage.getByRole('button', { name: 'Share' }), - ).toBeVisible(); const card = otherPage.getByLabel('It is the card information'); await expect(card).toBeVisible(); await expect(card.getByText('Reader')).toBeVisible(); @@ -266,17 +259,6 @@ test.describe('Doc Visibility: Public', () => { await writeInEditor({ page, text: 'Can you see it ?' }); await expect(otherEditor.getByText('Can you see it ?')).toBeVisible(); - await otherPage.getByRole('button', { name: 'Share' }).click(); - await expect( - otherPage.getByText( - 'You can view this document but need additional access to see its members or modify settings.', - ), - ).toBeVisible(); - - await expect( - otherPage.getByRole('button', { name: 'Request access' }), - ).toBeHidden(); - await cleanup(); }); @@ -313,11 +295,7 @@ test.describe('Doc Visibility: Public', () => { 'It is the card information about the document.', ); - await expect(cardContainer.getByTestId('public-icon')).toBeVisible(); - - await expect( - cardContainer.getByText('Public document', { exact: true }), - ).toBeVisible(); + await expect(cardContainer.getByText('Public ·')).toBeVisible(); const docUrl = page.url(); @@ -341,24 +319,10 @@ test.describe('Doc Visibility: Public', () => { page.locator('.collaboration-cursor-custom__base').getByText('Anonymous'), ).toBeVisible(); - await expect( - otherPage.getByRole('button', { name: 'Share' }), - ).toBeVisible(); const card = otherPage.getByLabel('It is the card information'); await expect(card).toBeVisible(); await expect(card.getByText('Editor')).toBeVisible(); - await otherPage.getByRole('button', { name: 'Share' }).click(); - await expect( - otherPage.getByText( - 'You can view this document but need additional access to see its members or modify settings.', - ), - ).toBeVisible(); - - await expect( - otherPage.getByRole('button', { name: 'Request access' }), - ).toBeHidden(); - await cleanup(); }); }); @@ -433,16 +397,14 @@ test.describe('Doc Visibility: Authenticated', () => { page.getByText('The document visibility has been updated.'), ).toBeVisible(); + await page.getByRole('button', { name: 'close' }).click(); + await expect( page .getByLabel('It is the card information about the document.') - .getByText('Document accessible to any connected person', { - exact: true, - }), + .getByText('Internal ·'), ).toBeVisible(); - await page.getByRole('button', { name: 'close' }).click(); - const docUrl = page.url(); const { name: childTitle } = await createRootSubPage( 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 782792fcb2..0e49902638 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -425,8 +425,18 @@ export async function waitForLanguageSwitch( await page.getByRole('menuitemradio', { name: lang.label }).click(); } +export const clickInEditorShareButton = async (page: Page) => { + await page + .getByTestId('floating-bar') + .getByRole('button', { name: 'Share' }) + .click(); +}; + export const clickInEditorMenu = async (page: Page, textButton: string) => { - await page.getByRole('button', { name: 'Open the document options' }).click(); + await page + .getByTestId('floating-bar') + .getByRole('button', { name: 'Open the document options' }) + .click(); await page.getByRole('menuitem', { name: textButton }).click(); }; diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/house-rounded.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/house-rounded.svg new file mode 100644 index 0000000000..28e2bcdbe9 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/house-rounded.svg @@ -0,0 +1,8 @@ + + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/plus.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/plus.svg new file mode 100644 index 0000000000..8f484184c1 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/plus.svg @@ -0,0 +1,6 @@ + + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/public.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/public.svg index 5c867ad131..3f8601d52e 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/public.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/public.svg @@ -1,3 +1,6 @@ - - + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/shared.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/shared.svg index 6d7c9f17a6..7097f5b671 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/shared.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/shared.svg @@ -1,6 +1,20 @@ - - - - - + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/vpn_lock.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/vpn_lock.svg index 6c05021ee0..438f2daffb 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/vpn_lock.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/vpn_lock.svg @@ -1,3 +1,6 @@ - - + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/zoom-rounded.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/zoom-rounded.svg new file mode 100644 index 0000000000..6868a5e452 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/zoom-rounded.svg @@ -0,0 +1,8 @@ + + + diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index 9c30742232..673c0668cc 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 0000000000..caa82a8da0 --- /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__/DocHeaderInfo.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx deleted file mode 100644 index 93f1a49913..0000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; -import { describe, expect, test, vi } from 'vitest'; - -import { AppWrapper } from '@/tests/utils'; - -// Force mobile layout so the children count is rendered -vi.mock('@/stores', () => ({ - useResponsiveStore: () => ({ isDesktop: false }), -})); - -// Provide stable mocks for hooks used by the component -vi.mock('../../doc-management', async () => { - const actual = await vi.importActual('../../doc-management'); - return { - ...actual, - useTrans: () => ({ transRole: vi.fn((r) => String(r)) }), - useIsCollaborativeEditable: () => ({ isEditable: true }), - }; -}); - -vi.mock('@/core', () => ({ - useConfig: () => ({ data: {} }), -})); - -vi.mock('@/hook', () => ({ - useDate: () => ({ - relativeDate: () => 'yesterday', - calculateDaysLeft: () => 5, - }), -})); - -import { DocHeaderInfo } from '../components/DocHeaderInfo'; - -describe('DocHeaderInfo', () => { - test('renders the number of sub-documents when numchild is provided (mobile layout)', () => { - const doc = { - numchild: 3, - updated_at: new Date().toISOString(), - } as any; - - render(, { wrapper: AppWrapper }); - - expect(screen.getByText(/Contains 3 sub-documents/i)).toBeInTheDocument(); - }); -}); 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 5e0024cce1..0000000000 --- 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 e0e693d31f..087940fa02 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} + + + ), + }; +}); + +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/AlertPublic.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertPublic.tsx deleted file mode 100644 index caf6328e74..0000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertPublic.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -import { Card, Icon, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; - -export const AlertPublic = ({ isPublicDoc }: { isPublicDoc: boolean }) => { - const { t } = useTranslation(); - const { spacingsTokens } = useCunninghamTheme(); - - return ( - - - - {isPublicDoc - ? t('Public document') - : t('Document accessible to any connected person')} - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertRestore.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertRestore.tsx index 4cfcf010a8..a25504c7a9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertRestore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertRestore.tsx @@ -67,7 +67,6 @@ export const AlertRestore = ({ doc }: { doc: Doc }) => { > diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/BoutonShare.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/BoutonShare.tsx deleted file mode 100644 index 9f86e08afb..0000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/BoutonShare.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { css } from 'styled-components'; - -import { Box, Icon } from '@/components'; -import { Doc } from '@/docs/doc-management'; -import { useFocusStore } from '@/stores'; - -interface BoutonShareProps { - displayNbAccess: boolean; - doc: Doc; - isDisabled?: boolean; - isHidden?: boolean; - open: () => void; -} - -export const BoutonShare = ({ - displayNbAccess, - doc, - isDisabled, - isHidden, - open, -}: BoutonShareProps) => { - const { t } = useTranslation(); - const addLastFocus = useFocusStore((state) => state.addLastFocus); - const treeContext = useTreeContext(); - - /** - * Following the change where there is no default owner when adding a sub-page, - * we need to handle both the case where the doc is the root and the case of sub-pages. - */ - const hasAccesses = useMemo(() => { - if (treeContext?.root?.id === doc.id) { - return doc.nb_accesses_direct > 1 && displayNbAccess; - } - - return doc.nb_accesses_direct >= 1 && displayNbAccess; - }, [doc.id, treeContext?.root, doc.nb_accesses_direct, displayNbAccess]); - - if (isHidden) { - return null; - } - - if (hasAccesses) { - return ( - - - - ); - } - - return ( - - ); -}; 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 85430234c0..2a71202ed8 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,82 +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, - LinkReach, - getDocLinkReach, + getEmojiAndTitle, + useDocTitleUpdate, + useDocUtils, useIsCollaborativeEditable, } from '@/docs/doc-management'; -import { useResponsiveStore } from '@/stores'; import { AlertNetwork } from './AlertNetwork'; -import { AlertPublic } from './AlertPublic'; import { AlertRestore } from './AlertRestore'; -import { BoutonShare } from './BoutonShare'; 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 docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC; - const docIsAuth = getDocLinkReach(doc) === LinkReach.AUTHENTICATED; 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 && } - {(docIsPublic || docIsAuth) && ( - - )} - - - - - - - - {!isDeletedDoc && } - {isDeletedDoc && ( - {}} - displayNbAccess={true} - isDisabled - /> + {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 fbc554201f..8de22bd4a4 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 @@ -1,30 +1,29 @@ import { t } from 'i18next'; -import React from 'react'; -import { Text } from '@/components'; +import PublicSVG from '@/assets/icons/ui-kit/public.svg'; +import ProtedtedSVG from '@/assets/icons/ui-kit/vpn_lock.svg'; +import { Box, Text } from '@/components'; import { useConfig } from '@/core'; import { Doc, + LinkReach, Role, + getDocLinkReach, useIsCollaborativeEditable, useTrans, } from '@/docs/doc-management'; import { useDate } from '@/hooks'; -import { useResponsiveStore } from '@/stores'; interface DocHeaderInfoProps { doc: Doc; } export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { - const { isDesktop } = useResponsiveStore(); const { transRole } = useTrans(); const { isEditable } = useIsCollaborativeEditable(doc); const { relativeDate, calculateDaysLeft } = useDate(); const { data: config } = useConfig(); - const childrenCount = doc.numchild ?? 0; - const relativeOnly = relativeDate(doc.updated_at); let dateToDisplay = t('Last update: {{update}}', { @@ -40,40 +39,45 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { dateToDisplay = `${t('Days remaining:')} ${daysLeft} ${t('days', { count: daysLeft })}`; } - const hasChildren = childrenCount > 0; + return ( + + + + {transRole(isEditable ? doc.user_role || doc.link_role : Role.READER)} +  ·  + + + {dateToDisplay} + + + ); +}; - if (isDesktop) { +const VisibilityDoc = ({ doc }: { doc: Doc }) => { + const docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC; + const docIsAuth = getDocLinkReach(doc) === LinkReach.AUTHENTICATED; + + if (docIsPublic) { return ( <> - - {transRole(isEditable ? doc.user_role || doc.link_role : Role.READER)} -  ·  - - - {dateToDisplay} - + ); }; 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 19973d6c28..78380ca3df 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,57 +1,40 @@ 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 GroupSVG from '@/assets/icons/ui-kit/group.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'; -import { BoutonShare } from './BoutonShare'; - -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) => ({ @@ -68,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( @@ -87,17 +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 modalShare = 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) => { @@ -111,25 +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('Share'), - icon: