From 1ae831cabd7467facaa6acab98e93d76e4a9ba1b Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 25 Jul 2025 13:26:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20search=20on=20al?= =?UTF-8?q?l=20docs=20if=20no=20children?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When searching for documents, if no children are found, the search will now include all documents instead of just those with children. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-search.spec.ts | 111 ++++++++++++------ .../docs/doc-management/hooks/index.ts | 1 + .../hooks/useDocUtils.tsx} | 3 +- .../doc-search/components/DocSearchModal.tsx | 47 +++++++- .../doc-share/components/DocVisibility.tsx | 4 +- .../src/features/docs/doc-tree/hooks/index.ts | 1 - .../src/features/docs/doc-tree/index.ts | 1 - .../left-panel/components/LeftPanelHeader.tsx | 11 +- 9 files changed, 126 insertions(+), 54 deletions(-) rename src/frontend/apps/impress/src/features/docs/{doc-tree/hooks/useTreeUtils.tsx => doc-management/hooks/useDocUtils.tsx} (80%) delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-tree/hooks/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ce8322..a2dc2524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to ### Changed +- ♻️(frontend) search on all docs if no children #1184 - ♻️(frontend) redirect to doc after duplicate #1175 - 🔧(project) change env.d system by using local files #1200 - ⚡️(frontend) improve tree stability #1207 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 1d55a07f..c8c09942 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,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { createDoc, randomName, verifyDocName } from './utils-common'; +import { createRootSubPage } from './utils-sub-pages'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -98,39 +99,46 @@ test.describe('Document search', () => { ).toBeHidden(); }); - test("it checks we don't see filters in search modal", async ({ page }) => { - const searchButton = page - .getByTestId('left-panel-desktop') - .getByRole('button', { name: 'search' }); - - await expect(searchButton).toBeVisible(); - await page.getByRole('button', { name: 'search', exact: true }).click(); - await expect( - page.getByRole('combobox', { name: 'Quick search input' }), - ).toBeVisible(); - await expect(page.getByTestId('doc-search-filters')).toBeHidden(); - }); -}); - -test.describe('Sub page search', () => { test('it check the presence of filters in search modal', async ({ page, browserName, }) => { - await page.goto('/'); - const [doc1Title] = await createDoc( - page, - 'My sub page search', - browserName, - 1, - ); - await verifyDocName(page, doc1Title); + // Doc grid filters are not visible const searchButton = page .getByTestId('left-panel-desktop') - .getByRole('button', { name: 'search' }); - await searchButton.click(); + .getByRole('button', { name: 'search', exact: true }); + const filters = page.getByTestId('doc-search-filters'); + + await searchButton.click(); + await expect( + page.getByRole('combobox', { name: 'Quick search input' }), + ).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: 'Quick search input' }), + ).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( @@ -139,43 +147,70 @@ test.describe('Sub page search', () => { await expect( page.getByRole('menuitem', { name: 'Current doc' }), ).toBeVisible(); - await page.getByRole('menuitem', { name: 'Current doc' }).click(); + await page.getByRole('menuitem', { name: 'All docs' }).click(); await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); }); test('it searches sub pages', async ({ page, browserName }) => { - await page.goto('/'); - - const [doc1Title] = await createDoc( + // First doc + const [firstDocTitle] = await createDoc( page, - 'My sub page search', + 'My first sub page search', browserName, 1, ); - await verifyDocName(page, doc1Title); + await verifyDocName(page, firstDocTitle); + + // Create a new doc - for the moment without children await page.getByRole('button', { name: 'New doc' }).click(); await verifyDocName(page, ''); await page.getByRole('textbox', { name: 'doc title input' }).click(); await page .getByRole('textbox', { name: 'doc title input' }) .press('ControlOrMeta+a'); - const [randomDocName] = randomName('doc-sub-page', browserName, 1); + const [secondDocTitle] = randomName( + 'My second sub page search', + browserName, + 1, + ); await page .getByRole('textbox', { name: 'doc title input' }) - .fill(randomDocName); + .fill(secondDocTitle); + const searchButton = page .getByTestId('left-panel-desktop') .getByRole('button', { name: 'search' }); await searchButton.click(); - await expect( - page.getByRole('button', { name: 'Current doc' }), - ).toBeVisible(); await page.getByRole('combobox', { name: 'Quick search input' }).click(); await page .getByRole('combobox', { name: 'Quick search input' }) - .fill('sub'); - await expect(page.getByLabel(randomDocName)).toBeVisible(); + .fill('sub page search'); + + // Expect to find the first doc + await expect( + page.getByRole('presentation').getByLabel(firstDocTitle), + ).toBeVisible(); + await expect( + page.getByRole('presentation').getByLabel(secondDocTitle), + ).toBeVisible(); + + await page.getByRole('button', { name: 'close' }).click(); + + // Create a sub page + await createRootSubPage(page, browserName, secondDocTitle); + await searchButton.click(); + await page + .getByRole('combobox', { name: 'Quick search input' }) + .fill('sub page search'); + + // Now there is a sub page - expect to have the focus on the current doc + await expect( + page.getByRole('presentation').getByLabel(secondDocTitle), + ).toBeVisible(); + await expect( + page.getByRole('presentation').getByLabel(firstDocTitle), + ).toBeHidden(); }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts index 96968e38..adf2d777 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts @@ -1,4 +1,5 @@ export * from './useCollaboration'; export * from './useCopyDocLink'; +export * from './useDocUtils'; export * from './useIsCollaborativeEditable'; export * from './useTrans'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeUtils.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useDocUtils.tsx similarity index 80% rename from src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeUtils.tsx rename to src/frontend/apps/impress/src/features/docs/doc-management/hooks/useDocUtils.tsx index 34f4d02c..3e954d9b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeUtils.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useDocUtils.tsx @@ -1,9 +1,10 @@ import { Doc } from '@/docs/doc-management'; -export const useTreeUtils = (doc: Doc) => { +export const useDocUtils = (doc: Doc) => { return { isTopRoot: doc.depth === 1, isChild: doc.depth > 1, + hasChildren: doc.numchild > 0, isDesynchronized: !!( doc.ancestors_link_reach && (doc.computed_link_reach !== doc.ancestors_link_reach || diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx index c8f1dbd1..b0137082 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx @@ -7,9 +7,9 @@ import { useDebouncedCallback } from 'use-debounce'; import { Box } from '@/components'; import { QuickSearch } from '@/components/quick-search'; +import { Doc, useDocUtils } from '@/docs/doc-management'; import { useResponsiveStore } from '@/stores'; -import { Doc } from '../../doc-management'; import EmptySearchIcon from '../assets/illustration-docs-empty.png'; import { DocSearchContent } from './DocSearchContent'; @@ -20,18 +20,18 @@ import { } from './DocSearchFilters'; import { DocSearchSubPageContent } from './DocSearchSubPageContent'; -type DocSearchModalProps = { +type DocSearchModalGlobalProps = { onClose: () => void; isOpen: boolean; showFilters?: boolean; defaultFilters?: DocSearchFiltersValues; }; -export const DocSearchModal = ({ +const DocSearchModalGlobal = ({ showFilters = false, defaultFilters, ...modalProps -}: DocSearchModalProps) => { +}: DocSearchModalGlobalProps) => { const { t } = useTranslation(); const [loading, setLoading] = useState(false); @@ -126,3 +126,42 @@ export const DocSearchModal = ({ ); }; + +type DocSearchModalDetailProps = DocSearchModalGlobalProps & { + doc: Doc; +}; + +const DocSearchModalDetail = ({ + doc, + ...modalProps +}: DocSearchModalDetailProps) => { + const { hasChildren, isChild } = useDocUtils(doc); + const isWithChildren = isChild || hasChildren; + + let defaultFilters = DocSearchTarget.ALL; + let showFilters = false; + if (isWithChildren) { + defaultFilters = DocSearchTarget.CURRENT; + showFilters = true; + } + + return ( + + ); +}; + +type DocSearchModalProps = DocSearchModalGlobalProps & { + doc?: Doc; +}; + +export const DocSearchModal = ({ doc, ...modalProps }: DocSearchModalProps) => { + if (doc) { + return ; + } + + return ; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx index 3ed24b14..36e97c2e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx @@ -17,9 +17,9 @@ import { LinkReach, LinkRole, getDocLinkReach, + useDocUtils, useUpdateDocLink, } from '@/docs/doc-management'; -import { useTreeUtils } from '@/docs/doc-tree'; import { useResponsiveStore } from '@/stores'; import { useTranslatedShareSettings } from '../hooks/'; @@ -37,7 +37,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { const canManage = doc.abilities.accesses_manage; const docLinkReach = getDocLinkReach(doc); const docLinkRole = doc.computed_link_role ?? LinkRole.READER; - const { isDesynchronized } = useTreeUtils(doc); + const { isDesynchronized } = useDocUtils(doc); const { linkModeTranslations, linkReachChoices, linkReachTranslations } = useTranslatedShareSettings(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/index.ts deleted file mode 100644 index 3fb57a34..00000000 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useTreeUtils'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts index ec8b4043..527c58f0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts @@ -1,4 +1,3 @@ export * from './api'; export * from './components'; -export * from './hooks'; export * from './utils'; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx index 5733b0df..d3621e06 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx @@ -3,7 +3,8 @@ import { useRouter } from 'next/router'; import { PropsWithChildren, useCallback, useState } from 'react'; import { Box, Icon, SeparatedSection } from '@/components'; -import { DocSearchModal, DocSearchTarget } from '@/docs/doc-search/'; +import { useDocStore } from '@/docs/doc-management'; +import { DocSearchModal } from '@/docs/doc-search/'; import { useAuth } from '@/features/auth'; import { useCmdK } from '@/hook/useCmdK'; @@ -12,10 +13,9 @@ import { useLeftPanelStore } from '../stores'; import { LeftPanelHeaderButton } from './LeftPanelHeaderButton'; export const LeftPanelHeader = ({ children }: PropsWithChildren) => { + const { currentDoc } = useDocStore(); const router = useRouter(); const { authenticated } = useAuth(); - const isDoc = router.pathname === '/docs/[id]'; - const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); const openSearchModal = useCallback(() => { @@ -81,10 +81,7 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { )}