♻️(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:
@@ -16,6 +16,7 @@ and this project adheres to
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- ♻️(frontend) search on all docs if no children #1184
|
||||||
- ♻️(frontend) redirect to doc after duplicate #1175
|
- ♻️(frontend) redirect to doc after duplicate #1175
|
||||||
- 🔧(project) change env.d system by using local files #1200
|
- 🔧(project) change env.d system by using local files #1200
|
||||||
- ⚡️(frontend) improve tree stability #1207
|
- ⚡️(frontend) improve tree stability #1207
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, randomName, verifyDocName } from './utils-common';
|
import { createDoc, randomName, verifyDocName } from './utils-common';
|
||||||
|
import { createRootSubPage } from './utils-sub-pages';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -98,39 +99,46 @@ test.describe('Document search', () => {
|
|||||||
).toBeHidden();
|
).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 ({
|
test('it check the presence of filters in search modal', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto('/');
|
// Doc grid filters are not visible
|
||||||
const [doc1Title] = await createDoc(
|
|
||||||
page,
|
|
||||||
'My sub page search',
|
|
||||||
browserName,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
await verifyDocName(page, doc1Title);
|
|
||||||
const searchButton = page
|
const searchButton = page
|
||||||
.getByTestId('left-panel-desktop')
|
.getByTestId('left-panel-desktop')
|
||||||
.getByRole('button', { name: 'search' });
|
.getByRole('button', { name: 'search', exact: true });
|
||||||
await searchButton.click();
|
|
||||||
const filters = page.getByTestId('doc-search-filters');
|
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 expect(filters).toBeVisible();
|
||||||
|
|
||||||
await filters.click();
|
await filters.click();
|
||||||
await filters.getByRole('button', { name: 'Current doc' }).click();
|
await filters.getByRole('button', { name: 'Current doc' }).click();
|
||||||
await expect(
|
await expect(
|
||||||
@@ -139,43 +147,70 @@ test.describe('Sub page search', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('menuitem', { name: 'Current doc' }),
|
page.getByRole('menuitem', { name: 'Current doc' }),
|
||||||
).toBeVisible();
|
).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();
|
await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it searches sub pages', async ({ page, browserName }) => {
|
test('it searches sub pages', async ({ page, browserName }) => {
|
||||||
await page.goto('/');
|
// First doc
|
||||||
|
const [firstDocTitle] = await createDoc(
|
||||||
const [doc1Title] = await createDoc(
|
|
||||||
page,
|
page,
|
||||||
'My sub page search',
|
'My first sub page search',
|
||||||
browserName,
|
browserName,
|
||||||
1,
|
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 page.getByRole('button', { name: 'New doc' }).click();
|
||||||
await verifyDocName(page, '');
|
await verifyDocName(page, '');
|
||||||
await page.getByRole('textbox', { name: 'doc title input' }).click();
|
await page.getByRole('textbox', { name: 'doc title input' }).click();
|
||||||
await page
|
await page
|
||||||
.getByRole('textbox', { name: 'doc title input' })
|
.getByRole('textbox', { name: 'doc title input' })
|
||||||
.press('ControlOrMeta+a');
|
.press('ControlOrMeta+a');
|
||||||
const [randomDocName] = randomName('doc-sub-page', browserName, 1);
|
const [secondDocTitle] = randomName(
|
||||||
|
'My second sub page search',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
await page
|
await page
|
||||||
.getByRole('textbox', { name: 'doc title input' })
|
.getByRole('textbox', { name: 'doc title input' })
|
||||||
.fill(randomDocName);
|
.fill(secondDocTitle);
|
||||||
|
|
||||||
const searchButton = page
|
const searchButton = page
|
||||||
.getByTestId('left-panel-desktop')
|
.getByTestId('left-panel-desktop')
|
||||||
.getByRole('button', { name: 'search' });
|
.getByRole('button', { name: 'search' });
|
||||||
|
|
||||||
await searchButton.click();
|
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' }).click();
|
||||||
await page
|
await page
|
||||||
.getByRole('combobox', { name: 'Quick search input' })
|
.getByRole('combobox', { name: 'Quick search input' })
|
||||||
.fill('sub');
|
.fill('sub page search');
|
||||||
await expect(page.getByLabel(randomDocName)).toBeVisible();
|
|
||||||
|
// 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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './useCollaboration';
|
export * from './useCollaboration';
|
||||||
export * from './useCopyDocLink';
|
export * from './useCopyDocLink';
|
||||||
|
export * from './useDocUtils';
|
||||||
export * from './useIsCollaborativeEditable';
|
export * from './useIsCollaborativeEditable';
|
||||||
export * from './useTrans';
|
export * from './useTrans';
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Doc } from '@/docs/doc-management';
|
import { Doc } from '@/docs/doc-management';
|
||||||
|
|
||||||
export const useTreeUtils = (doc: Doc) => {
|
export const useDocUtils = (doc: Doc) => {
|
||||||
return {
|
return {
|
||||||
isTopRoot: doc.depth === 1,
|
isTopRoot: doc.depth === 1,
|
||||||
isChild: doc.depth > 1,
|
isChild: doc.depth > 1,
|
||||||
|
hasChildren: doc.numchild > 0,
|
||||||
isDesynchronized: !!(
|
isDesynchronized: !!(
|
||||||
doc.ancestors_link_reach &&
|
doc.ancestors_link_reach &&
|
||||||
(doc.computed_link_reach !== doc.ancestors_link_reach ||
|
(doc.computed_link_reach !== doc.ancestors_link_reach ||
|
||||||
@@ -7,9 +7,9 @@ import { useDebouncedCallback } from 'use-debounce';
|
|||||||
|
|
||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
import { QuickSearch } from '@/components/quick-search';
|
import { QuickSearch } from '@/components/quick-search';
|
||||||
|
import { Doc, useDocUtils } from '@/docs/doc-management';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { Doc } from '../../doc-management';
|
|
||||||
import EmptySearchIcon from '../assets/illustration-docs-empty.png';
|
import EmptySearchIcon from '../assets/illustration-docs-empty.png';
|
||||||
|
|
||||||
import { DocSearchContent } from './DocSearchContent';
|
import { DocSearchContent } from './DocSearchContent';
|
||||||
@@ -20,18 +20,18 @@ import {
|
|||||||
} from './DocSearchFilters';
|
} from './DocSearchFilters';
|
||||||
import { DocSearchSubPageContent } from './DocSearchSubPageContent';
|
import { DocSearchSubPageContent } from './DocSearchSubPageContent';
|
||||||
|
|
||||||
type DocSearchModalProps = {
|
type DocSearchModalGlobalProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
showFilters?: boolean;
|
showFilters?: boolean;
|
||||||
defaultFilters?: DocSearchFiltersValues;
|
defaultFilters?: DocSearchFiltersValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocSearchModal = ({
|
const DocSearchModalGlobal = ({
|
||||||
showFilters = false,
|
showFilters = false,
|
||||||
defaultFilters,
|
defaultFilters,
|
||||||
...modalProps
|
...modalProps
|
||||||
}: DocSearchModalProps) => {
|
}: DocSearchModalGlobalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -126,3 +126,42 @@ export const DocSearchModal = ({
|
|||||||
</Modal>
|
</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} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import {
|
|||||||
LinkReach,
|
LinkReach,
|
||||||
LinkRole,
|
LinkRole,
|
||||||
getDocLinkReach,
|
getDocLinkReach,
|
||||||
|
useDocUtils,
|
||||||
useUpdateDocLink,
|
useUpdateDocLink,
|
||||||
} from '@/docs/doc-management';
|
} from '@/docs/doc-management';
|
||||||
import { useTreeUtils } from '@/docs/doc-tree';
|
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { useTranslatedShareSettings } from '../hooks/';
|
import { useTranslatedShareSettings } from '../hooks/';
|
||||||
@@ -37,7 +37,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
|||||||
const canManage = doc.abilities.accesses_manage;
|
const canManage = doc.abilities.accesses_manage;
|
||||||
const docLinkReach = getDocLinkReach(doc);
|
const docLinkReach = getDocLinkReach(doc);
|
||||||
const docLinkRole = doc.computed_link_role ?? LinkRole.READER;
|
const docLinkRole = doc.computed_link_role ?? LinkRole.READER;
|
||||||
const { isDesynchronized } = useTreeUtils(doc);
|
const { isDesynchronized } = useDocUtils(doc);
|
||||||
const { linkModeTranslations, linkReachChoices, linkReachTranslations } =
|
const { linkModeTranslations, linkReachChoices, linkReachTranslations } =
|
||||||
useTranslatedShareSettings();
|
useTranslatedShareSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './useTreeUtils';
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './api';
|
export * from './api';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './hooks';
|
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useRouter } from 'next/router';
|
|||||||
import { PropsWithChildren, useCallback, useState } from 'react';
|
import { PropsWithChildren, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { Box, Icon, SeparatedSection } from '@/components';
|
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 { useAuth } from '@/features/auth';
|
||||||
import { useCmdK } from '@/hook/useCmdK';
|
import { useCmdK } from '@/hook/useCmdK';
|
||||||
|
|
||||||
@@ -12,10 +13,9 @@ import { useLeftPanelStore } from '../stores';
|
|||||||
import { LeftPanelHeaderButton } from './LeftPanelHeaderButton';
|
import { LeftPanelHeaderButton } from './LeftPanelHeaderButton';
|
||||||
|
|
||||||
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||||
|
const { currentDoc } = useDocStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { authenticated } = useAuth();
|
const { authenticated } = useAuth();
|
||||||
const isDoc = router.pathname === '/docs/[id]';
|
|
||||||
|
|
||||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
||||||
|
|
||||||
const openSearchModal = useCallback(() => {
|
const openSearchModal = useCallback(() => {
|
||||||
@@ -81,10 +81,7 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
|||||||
<DocSearchModal
|
<DocSearchModal
|
||||||
onClose={closeSearchModal}
|
onClose={closeSearchModal}
|
||||||
isOpen={isSearchModalOpen}
|
isOpen={isSearchModalOpen}
|
||||||
showFilters={isDoc}
|
doc={currentDoc}
|
||||||
defaultFilters={{
|
|
||||||
target: isDoc ? DocSearchTarget.CURRENT : undefined,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user