♻️(frontend) search on all docs if no children

When searching for documents, if no children are
found, the search will now include all documents
instead of just those with children.
This commit is contained in:
Anthony LC
2025-07-25 13:26:39 +02:00
parent f1c2219270
commit 1ae831cabd
9 changed files with 126 additions and 54 deletions

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -1,4 +1,5 @@
export * from './useCollaboration';
export * from './useCopyDocLink';
export * from './useDocUtils';
export * from './useIsCollaborativeEditable';
export * from './useTrans';

View File

@@ -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 ||

View File

@@ -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 = ({
</Modal>
);
};
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 (
<DocSearchModalGlobal
{...modalProps}
showFilters={showFilters}
defaultFilters={{ target: defaultFilters }}
/>
);
};
type DocSearchModalProps = DocSearchModalGlobalProps & {
doc?: Doc;
};
export const DocSearchModal = ({ doc, ...modalProps }: DocSearchModalProps) => {
if (doc) {
return <DocSearchModalDetail doc={doc} {...modalProps} />;
}
return <DocSearchModalGlobal {...modalProps} />;
};

View File

@@ -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();

View File

@@ -1 +0,0 @@
export * from './useTreeUtils';

View File

@@ -1,4 +1,3 @@
export * from './api';
export * from './components';
export * from './hooks';
export * from './utils';

View File

@@ -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) => {
<DocSearchModal
onClose={closeSearchModal}
isOpen={isSearchModalOpen}
showFilters={isDoc}
defaultFilters={{
target: isDoc ? DocSearchTarget.CURRENT : undefined,
}}
doc={currentDoc}
/>
)}
</>