From 6dc9955d6442d796232bc90d5e71c1f57d939ade Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 8 Jun 2026 17:39:12 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20unauthenticated=20users?= =?UTF-8?q?=20can=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unauthenticated users can now search documents inside a document depend their permissions on the document. --- CHANGELOG.md | 4 + .../__tests__/app-impress/doc-search.spec.ts | 123 ++++++++++-------- .../__tests__/app-impress/doc-tree.spec.ts | 16 ++- .../app-impress/doc-visibility.spec.ts | 3 - .../doc-management/api/useCreateChildDoc.tsx | 4 + .../docs/doc-management/api/useCreateDoc.tsx | 4 + .../features/docs/doc-management/types.tsx | 1 + .../components/DocSearchButtonModal.tsx | 65 +++++++++ .../doc-search/components/DocSearchModal.tsx | 14 +- .../left-panel/components/LeftPanelHeader.tsx | 107 ++++----------- .../service-worker/plugins/ApiPlugin.ts | 1 + 11 files changed, 191 insertions(+), 151 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a4d0e5ee..6d5d10801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to ## [Unreleased] +### Added + +- ✨(frontend) unauthenticated users can search #2407 + ### Changed - 👷(CI) remove test-e2e-other-browser job #2404 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts index 7497d6737..31f3a67a7 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts @@ -1,7 +1,11 @@ import { expect, test } from '@playwright/test'; import { createDoc, verifyDocName } from './utils-common'; -import { createRootSubPage } from './utils-sub-pages'; +import { connectOtherUserToDoc, updateShareLink } from './utils-share'; +import { + createRootSubPage, + navigateToTopParentFromTree, +} from './utils-sub-pages'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -96,57 +100,6 @@ test.describe('Document search', () => { ).toBeHidden(); }); - test('it check the presence of filters in search modal', async ({ - page, - browserName, - }) => { - // Doc grid filters are not visible - const searchButton = page.getByTestId('search-docs-button'); - - const filters = page.getByTestId('doc-search-filters'); - - await searchButton.click(); - await expect( - page.getByRole('combobox', { name: 'Search documents' }), - ).toBeVisible(); - await expect(filters).toBeHidden(); - - await page.getByRole('button', { name: 'close' }).click(); - - // Create a doc without children for the moment - // and check that filters are not visible - const [doc1Title] = await createDoc(page, 'My page search', browserName, 1); - await verifyDocName(page, doc1Title); - - await searchButton.click(); - await expect( - page.getByRole('combobox', { name: 'Search documents' }), - ).toBeVisible(); - await expect(filters).toBeHidden(); - - await page.getByRole('button', { name: 'close' }).click(); - - // Create a sub page - // and check that filters are visible - await createRootSubPage(page, browserName, 'My sub page search'); - - await searchButton.click(); - - await expect(filters).toBeVisible(); - - await filters.click(); - await filters.getByRole('button', { name: 'Current doc' }).click(); - await expect( - page.getByRole('menuitemcheckbox', { name: 'All docs' }), - ).toBeVisible(); - await expect( - page.getByRole('menuitemcheckbox', { name: 'Current doc' }), - ).toBeVisible(); - await page.getByRole('menuitemcheckbox', { name: 'All docs' }).click(); - - await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); - }); - test('it searches sub pages', async ({ page, browserName }) => { // First doc const [firstDocTitle] = await createDoc( @@ -171,7 +124,7 @@ test.describe('Document search', () => { await page.getByRole('combobox', { name: 'Search documents' }).click(); await page .getByRole('combobox', { name: 'Search documents' }) - .fill('sub page search'); + .fill('sub page'); // Expect to find the first and second docs in the results list const resultsList = page.getByRole('listbox'); @@ -188,14 +141,14 @@ test.describe('Document search', () => { const { name: secondChildDocTitle } = await createRootSubPage( page, browserName, - 'second - Child doc', + 'My second sub page search - Child doc', ); await searchButton.click(); await page .getByRole('combobox', { name: 'Search documents' }) - .fill('second'); + .fill('sub page'); - // Now there is a sub page - expect to have the focus on the current doc + // Display only current doc results const updatedResultsList = page.getByRole('listbox'); await expect( updatedResultsList.getByRole('option', { name: secondDocTitle }), @@ -206,5 +159,63 @@ test.describe('Document search', () => { await expect( updatedResultsList.getByRole('option', { name: firstDocTitle }), ).toBeHidden(); + + // Click on the filter to show all docs + const filters = page.getByTestId('doc-search-filters'); + await filters.click(); + await filters.getByRole('button', { name: 'Current doc' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'All docs' }).click(); + + // Expect to see all docs in the results list + await expect( + updatedResultsList.getByRole('option', { name: secondDocTitle }), + ).toBeVisible(); + await expect( + updatedResultsList.getByRole('option', { name: secondChildDocTitle }), + ).toBeVisible(); + await expect( + updatedResultsList.getByRole('option', { name: firstDocTitle }), + ).toBeVisible(); + + await page.getByRole('button', { name: 'close' }).click(); + + // Navigate to the top parent doc and make it public + await navigateToTopParentFromTree({ page }); + await verifyDocName(page, secondDocTitle); + await page.locator('[data-test="share-button"]').click(); + await updateShareLink(page, 'Public'); + await page.getByRole('button', { name: 'close' }).click(); + + const docUrl = page.url(); + + const { otherPage, cleanup } = await connectOtherUserToDoc({ + browserName, + docUrl, + docTitle: secondDocTitle, + withoutSignIn: true, + }); + + await otherPage.getByTestId('search-docs-button').click(); + await otherPage + .getByRole('combobox', { name: 'Search documents' }) + .fill('sub page'); + + // Search only in the current doc + const otherPageResultsList = otherPage.getByRole('listbox'); + await expect( + otherPageResultsList.getByRole('option', { name: secondDocTitle }), + ).toBeVisible(); + await expect( + otherPageResultsList.getByRole('option', { name: secondChildDocTitle }), + ).toBeVisible(); + await expect( + otherPageResultsList.getByRole('option', { name: firstDocTitle }), + ).toBeHidden(); + + // Filter is not displayed because the user is not connected + const otherPageFilters = otherPage.getByTestId('doc-search-filters'); + await expect(otherPageFilters).toBeHidden(); + + await cleanup(); }); }); 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 b852a32be..060eb4a33 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 @@ -179,13 +179,14 @@ test.describe('Doc Tree', () => { await expect(allSubPageItems.nth(1).getByText('second move')).toBeVisible(); // Will move the first sub page to the second position - // Use the testId-based locators for bounding box to avoid stale text locators + // Wait for elements to be stable before reading their positions — a React + // re-render can transiently detach nodes, making boundingBox() return null. + await allSubPageItems.nth(0).waitFor({ state: 'visible' }); + await allSubPageItems.nth(1).waitFor({ state: 'visible' }); + const firstSubPageBoundingBox = await allSubPageItems.nth(0).boundingBox(); const secondSubPageBoundingBox = await allSubPageItems.nth(1).boundingBox(); - expect(firstSubPageBoundingBox).toBeDefined(); - expect(secondSubPageBoundingBox).toBeDefined(); - if (!firstSubPageBoundingBox || !secondSubPageBoundingBox) { throw new Error('unable to determine the position of the elements'); } @@ -205,9 +206,10 @@ test.describe('Doc Tree', () => { await page.mouse.up(); - // check that the sub pages are visible in the tree - await expect(firstSubPageItem).toBeVisible(); - await expect(secondSubPageItem).toBeVisible(); + // Wait for the reorder to be reflected in the tree before reloading — + // this also ensures the API call has had time to persist the new order. + await expect(allSubPageItems.nth(0).getByText('second move')).toBeVisible(); + await expect(allSubPageItems.nth(1).getByText('first move')).toBeVisible(); // reload the page await page.reload(); 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 a057b5140..6dca8b40b 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 @@ -222,7 +222,6 @@ test.describe('Doc Visibility: Public', () => { await expect(cardContainer.getByText('Public ·')).toBeVisible(); - await expect(page.getByTestId('search-docs-button')).toBeVisible(); await expect(page.getByTestId('new-doc-button')).toBeVisible(); const docUrl = page.url(); @@ -234,7 +233,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(); const card = otherPage.getByLabel('It is the card information'); await expect(card).toBeVisible(); @@ -306,7 +304,6 @@ test.describe('Doc Visibility: Public', () => { docTitle, }); - await expect(otherPage.getByTestId('search-docs-button')).toBeHidden(); await expect(otherPage.getByTestId('new-doc-button')).toBeHidden(); const otherEditor = await getEditor({ page: otherPage }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateChildDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateChildDoc.tsx index d6ad9aaf3..c8b6ff2ee 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateChildDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateChildDoc.tsx @@ -5,6 +5,7 @@ import { APIError, errorCauses, fetchAPI } from '@/api'; import { Doc } from '../types'; import { KEY_LIST_DOC } from './useDocs'; +import { KEY_LIST_SEARCH_DOC } from './useSearchDocs'; export type CreateChildDocParam = Pick & { parentId: string; @@ -40,6 +41,9 @@ export function useCreateChildDoc({ onSuccess }: UseCreateChildDocProps) { void queryClient.resetQueries({ queryKey: [KEY_LIST_DOC], }); + void queryClient.resetQueries({ + queryKey: [KEY_LIST_SEARCH_DOC], + }); onSuccess(doc); }, }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx index 4587e21ee..d0dbb48b6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx @@ -9,6 +9,7 @@ import { APIError, errorCauses, fetchAPI } from '@/api'; import { Doc } from '../types'; import { KEY_LIST_DOC } from './useDocs'; +import { KEY_LIST_SEARCH_DOC } from './useSearchDocs'; type CreateDocParams = { title?: string; @@ -37,6 +38,9 @@ export function useCreateDoc(options?: UseCreateDocOptions) { void queryClient.resetQueries({ queryKey: [KEY_LIST_DOC], }); + void queryClient.resetQueries({ + queryKey: [KEY_LIST_SEARCH_DOC], + }); options?.onSuccess?.(data, variables, onMutateResult, context); }, }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx index 44f800cc8..ebe5d983e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx @@ -94,6 +94,7 @@ export interface Doc { partial_update: boolean; restore: boolean; retrieve: boolean; + search: boolean; update: boolean; versions_destroy: boolean; versions_list: boolean; diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx new file mode 100644 index 000000000..02d95052f --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx @@ -0,0 +1,65 @@ +import { Button } from '@gouvfr-lasuite/cunningham-react'; +import { t } from 'i18next'; +import dynamic from 'next/dynamic'; +import { useCallback, useState } from 'react'; + +import SearchSVG from '@/assets/icons/ui-kit/zoom-rounded.svg'; +import { useDocStore } from '@/docs/doc-management'; +import { useAuth } from '@/features/auth'; +import { useCmdK } from '@/hooks/useCmdK'; + +const DocSearchModal = dynamic( + () => + import('./DocSearchModal').then((mod) => ({ + default: mod.DocSearchModal, + })), + { ssr: false }, +); + +export const DocSearchButtonModal = () => { + const { currentDoc } = useDocStore(); + const { authenticated } = useAuth(); + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); + const canSearch = authenticated || currentDoc?.abilities.search; + + const openSearchModal = useCallback(() => { + const isEditorToolbarOpen = + document.getElementsByClassName('bn-formatting-toolbar').length > 0; + if (isEditorToolbarOpen) { + return; + } + + setIsSearchModalOpen(true); + }, []); + + const closeSearchModal = useCallback(() => { + setIsSearchModalOpen(false); + }, []); + + useCmdK(openSearchModal); + + if (!canSearch) { + return null; + } + + return ( + <> +