mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
✨(frontend) add top parent on sub docs search
In the search modal, under the sub docs, we now display the top parent of the doc, to give more context to the user about the doc they are looking for. We refactorize the filters to get more flexibility and avoid too much props drilling.
This commit is contained in:
@@ -8,6 +8,7 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add top parent on sub docs search #1952
|
||||
- ✨(frontend) unauthenticated users can search #2407
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -14,7 +14,7 @@ export type QuickSearchAction = {
|
||||
};
|
||||
|
||||
export type QuickSearchData<T> = {
|
||||
groupName: string;
|
||||
groupName?: string;
|
||||
groupKey?: string;
|
||||
elements: T[];
|
||||
emptyString?: string;
|
||||
|
||||
@@ -16,6 +16,7 @@ export const QuickSearchStyle = createGlobalStyle`
|
||||
}
|
||||
|
||||
[cmdk-input] {
|
||||
font-family: var(--c--globals--font--families--base);
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
|
||||
@@ -10,4 +10,3 @@ export * from './useDuplicateDoc';
|
||||
export * from './useMoveDoc';
|
||||
export * from './useRestoreDoc';
|
||||
export * from './useUpdateDoc';
|
||||
export * from './useSearchDocs';
|
||||
|
||||
+42
-16
@@ -1,10 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import ArrowSVG from '@/assets/icons/ui-kit/subdirectory_arrow_right.svg';
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useDate } from '@/hooks/useDate';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import ChildDocument from '../assets/child-document.svg';
|
||||
import PinnedDocumentIcon from '../assets/pinned-document.svg';
|
||||
@@ -20,22 +20,24 @@ const ItemTextCss = css`
|
||||
line-clamp: 1;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type SimpleDocItemProps = {
|
||||
doc: Doc;
|
||||
breadcrumb?: string;
|
||||
isPinned?: boolean;
|
||||
showAccesses?: boolean;
|
||||
showDate?: boolean;
|
||||
};
|
||||
|
||||
export const SimpleDocItem = ({
|
||||
doc,
|
||||
isPinned = false,
|
||||
showAccesses = false,
|
||||
showDate = false,
|
||||
breadcrumb,
|
||||
}: SimpleDocItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { untitledDocument } = useTrans();
|
||||
const { isChild } = useDocUtils(doc);
|
||||
const { relativeDate, formatDate } = useDate();
|
||||
@@ -90,7 +92,7 @@ export const SimpleDocItem = ({
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box $justify="center" $overflow="auto">
|
||||
<Box $justify="center" $overflow="auto" $gap="3xs">
|
||||
<Text
|
||||
$size="sm"
|
||||
$weight="500"
|
||||
@@ -99,17 +101,41 @@ export const SimpleDocItem = ({
|
||||
>
|
||||
{docTitle}
|
||||
</Text>
|
||||
{(!isDesktop || showAccesses) && (
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$margin={{ top: '-2px' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Text $size="xs" $variation="tertiary">
|
||||
{docRelativeUpdate}
|
||||
</Text>
|
||||
|
||||
{(showDate || breadcrumb) && (
|
||||
<Box $direction="row" $align="center" aria-hidden="true">
|
||||
{breadcrumb && (
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="3xs"
|
||||
$css={css`
|
||||
& > svg {
|
||||
margin-top: -2px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ArrowSVG
|
||||
width="16px"
|
||||
height="16px"
|
||||
aria-hidden="true"
|
||||
color="var(--c--contextuals--content--semantic--neutral--tertiary)"
|
||||
/>
|
||||
<Text $size="xs" $variation="tertiary" $css={ItemTextCss}>
|
||||
{breadcrumb}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{breadcrumb && showDate && (
|
||||
<Text $size="xs" $variation="tertiary">
|
||||
·
|
||||
</Text>
|
||||
)}
|
||||
{showDate && (
|
||||
<Text $size="xs" $variation="tertiary">
|
||||
{docRelativeUpdate}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
+19
-12
@@ -8,26 +8,27 @@ import {
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
import { Doc } from '@/docs/doc-management';
|
||||
import { DocSearchTarget } from '@/docs/doc-search';
|
||||
|
||||
import { DocSearchFilterTypes } from '../types';
|
||||
|
||||
export type SearchDocsParams = {
|
||||
page: number;
|
||||
q: string;
|
||||
target?: DocSearchTarget;
|
||||
filter?: DocSearchFilterTypes;
|
||||
parentPath?: string;
|
||||
};
|
||||
|
||||
const constructParams = ({
|
||||
q,
|
||||
page,
|
||||
target,
|
||||
filter,
|
||||
parentPath,
|
||||
}: SearchDocsParams): URLSearchParams => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.set('q', q);
|
||||
|
||||
if (target === DocSearchTarget.CURRENT && parentPath) {
|
||||
if (filter === 'current' && parentPath) {
|
||||
searchParams.set('path', parentPath);
|
||||
}
|
||||
if (page) {
|
||||
@@ -37,13 +38,19 @@ const constructParams = ({
|
||||
return searchParams;
|
||||
};
|
||||
|
||||
export type DocSearch = Doc & {
|
||||
parent: Doc | null;
|
||||
};
|
||||
|
||||
type SearchDocsResponse = APIList<DocSearch>;
|
||||
|
||||
const searchDocs = async ({
|
||||
q,
|
||||
page,
|
||||
target,
|
||||
filter,
|
||||
parentPath,
|
||||
}: SearchDocsParams): Promise<APIList<Doc>> => {
|
||||
const searchParams = constructParams({ q, page, target, parentPath });
|
||||
}: SearchDocsParams): Promise<SearchDocsResponse> => {
|
||||
const searchParams = constructParams({ q, page, filter, parentPath });
|
||||
const response = await fetchAPI(
|
||||
`documents/search/?${searchParams.toString()}`,
|
||||
);
|
||||
@@ -52,18 +59,18 @@ const searchDocs = async ({
|
||||
throw new APIError('Failed to get the docs', await errorCauses(response));
|
||||
}
|
||||
|
||||
return response.json() as Promise<APIList<Doc>>;
|
||||
return response.json() as Promise<SearchDocsResponse>;
|
||||
};
|
||||
|
||||
export const KEY_LIST_SEARCH_DOC = 'search-docs';
|
||||
|
||||
export const useSearchDocs = (
|
||||
{ q, page, target, parentPath }: SearchDocsParams,
|
||||
param: SearchDocsParams,
|
||||
queryConfig?: { enabled?: boolean },
|
||||
) => {
|
||||
return useQuery<APIList<Doc>, APIError, APIList<Doc>>({
|
||||
queryKey: [KEY_LIST_SEARCH_DOC, 'search', { q, page, target, parentPath }],
|
||||
queryFn: () => searchDocs({ q, page, target, parentPath }),
|
||||
return useQuery<SearchDocsResponse, APIError, SearchDocsResponse>({
|
||||
queryKey: [KEY_LIST_SEARCH_DOC, param],
|
||||
queryFn: () => searchDocs(param),
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
+1
@@ -58,6 +58,7 @@ export const DocSearchButtonModal = () => {
|
||||
onClose={closeSearchModal}
|
||||
isOpen={isSearchModalOpen}
|
||||
doc={currentDoc}
|
||||
defaultFilters="all"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+11
-14
@@ -5,24 +5,22 @@ import { InView } from 'react-intersection-observer';
|
||||
|
||||
import { Box } from '@/components/';
|
||||
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
|
||||
import { useInfiniteSearchDocs } from '@/docs/doc-management/api/useSearchDocs';
|
||||
import { DocSearchTarget } from '@/docs/doc-search';
|
||||
|
||||
import { Doc } from '../../doc-management';
|
||||
import { DocSearch, useInfiniteSearchDocs } from '../api/useSearchDocs';
|
||||
import { useDocSearchFilterStore } from '../stores/useDocSearchFilterStore';
|
||||
|
||||
import { DocSearchItem } from './DocSearchItem';
|
||||
|
||||
type DocSearchContentProps = {
|
||||
groupName: string;
|
||||
groupName?: string;
|
||||
search: string;
|
||||
filterResults?: (doc: Doc) => boolean;
|
||||
filterResults?: (doc: DocSearch) => boolean;
|
||||
isSearchNotMandatory?: boolean;
|
||||
onResults?: (results: Doc[]) => void;
|
||||
onSelect: (doc: Doc) => void;
|
||||
onResults?: (results: DocSearch[]) => void;
|
||||
onSelect: (doc: DocSearch) => void;
|
||||
onLoadingChange?: (loading: boolean) => void;
|
||||
target?: DocSearchTarget;
|
||||
parentPath?: string;
|
||||
renderSearchElement?: (doc: Doc) => React.ReactNode;
|
||||
renderSearchElement?: (doc: DocSearch) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const DocSearchContent = ({
|
||||
@@ -33,10 +31,10 @@ export const DocSearchContent = ({
|
||||
onSelect,
|
||||
onLoadingChange,
|
||||
renderSearchElement,
|
||||
target,
|
||||
parentPath,
|
||||
isSearchNotMandatory,
|
||||
}: DocSearchContentProps) => {
|
||||
const { filter } = useDocSearchFilterStore();
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
@@ -48,16 +46,16 @@ export const DocSearchContent = ({
|
||||
{
|
||||
q: search,
|
||||
page: 1,
|
||||
target,
|
||||
filter,
|
||||
parentPath,
|
||||
},
|
||||
{
|
||||
enabled: target !== DocSearchTarget.CURRENT || !!parentPath,
|
||||
enabled: filter !== 'current' || !!parentPath,
|
||||
},
|
||||
);
|
||||
|
||||
const loading = isFetching || isRefetching || isLoading;
|
||||
const [docsData, setDocsData] = useState<QuickSearchData<Doc>>({
|
||||
const [docsData, setDocsData] = useState<QuickSearchData<DocSearch>>({
|
||||
groupName: '',
|
||||
groupKey: 'docs',
|
||||
elements: [],
|
||||
@@ -84,7 +82,6 @@ export const DocSearchContent = ({
|
||||
groupName: groupName,
|
||||
groupKey: 'docs',
|
||||
elements,
|
||||
emptyString: t('No document found'),
|
||||
endActions: hasNextPage
|
||||
? [
|
||||
{
|
||||
|
||||
+39
-60
@@ -1,73 +1,52 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Switch } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { FilterDropdown } from '@/components/filter/FilterDropdown';
|
||||
|
||||
export enum DocSearchTarget {
|
||||
ALL = 'all',
|
||||
CURRENT = 'current',
|
||||
}
|
||||
import { useDocSearchFilterStore } from '../stores/useDocSearchFilterStore';
|
||||
|
||||
export type DocSearchFiltersValues = {
|
||||
target?: DocSearchTarget;
|
||||
};
|
||||
|
||||
export type DocSearchFiltersProps = {
|
||||
values?: DocSearchFiltersValues;
|
||||
onValuesChange?: (values: DocSearchFiltersValues) => void;
|
||||
onReset?: () => void;
|
||||
};
|
||||
|
||||
export const DocSearchFilters = ({
|
||||
values,
|
||||
onValuesChange,
|
||||
onReset,
|
||||
}: DocSearchFiltersProps) => {
|
||||
export const DocSearchFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const hasFilters = Object.keys(values ?? {}).length > 0;
|
||||
const handleTargetChange = (target: DocSearchTarget) => {
|
||||
onValuesChange?.({ ...values, target });
|
||||
};
|
||||
const { setFilter, filter } = useDocSearchFilterStore();
|
||||
|
||||
return (
|
||||
/**
|
||||
* The switch is not focusable, so we wrap it in a div that can be focused
|
||||
* and handle the keydown event to toggle the switch with
|
||||
* space key for accessibility reasons
|
||||
*/
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$height="35px"
|
||||
$justify="space-between"
|
||||
$gap="10px"
|
||||
data-testid="doc-search-filters"
|
||||
$margin={{ vertical: 'base' }}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setFilter(filter === 'all' ? 'current' : 'all');
|
||||
}
|
||||
}}
|
||||
$css={css`
|
||||
&:focus-visible .c__switch__rail {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px
|
||||
var(--c--contextuals--border--semantic--brand--primary);
|
||||
}
|
||||
// Remove the default focus style of the switch component
|
||||
.c__checkbox:focus-within {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap="10px">
|
||||
<FilterDropdown
|
||||
selectedValue={values?.target}
|
||||
options={[
|
||||
{
|
||||
label: t('All docs'),
|
||||
value: DocSearchTarget.ALL,
|
||||
callback: () => handleTargetChange(DocSearchTarget.ALL),
|
||||
},
|
||||
{
|
||||
label: t('Current doc'),
|
||||
value: DocSearchTarget.CURRENT,
|
||||
callback: () => handleTargetChange(DocSearchTarget.CURRENT),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
{hasFilters && (
|
||||
<Button
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={onReset}
|
||||
aria-label={t('Reset search filters')}
|
||||
>
|
||||
{t('Reset')}
|
||||
</Button>
|
||||
)}
|
||||
<Switch
|
||||
labelSide="right"
|
||||
label={t('All docs')}
|
||||
checked={filter === 'all'}
|
||||
onChange={() => setFilter(filter === 'all' ? 'current' : 'all')}
|
||||
aria-label={t(
|
||||
'Toggle to search in all documents or only in current document',
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
+16
-4
@@ -1,14 +1,20 @@
|
||||
import ArrowIcon from '@/assets/icons/ui-kit/enter.svg';
|
||||
import { Box, Icon } from '@/components';
|
||||
import { QuickSearchItemContent } from '@/components/quick-search/';
|
||||
import { Doc, SimpleDocItem } from '@/docs/doc-management';
|
||||
import { SimpleDocItem } from '@/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocSearch } from '../api/useSearchDocs';
|
||||
import { useDocSearchFilterStore } from '../stores/useDocSearchFilterStore';
|
||||
|
||||
type DocSearchItemProps = {
|
||||
doc: Doc;
|
||||
doc: DocSearch;
|
||||
};
|
||||
|
||||
export const DocSearchItem = ({ doc }: DocSearchItemProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { filter } = useDocSearchFilterStore();
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-testid={`doc-search-item-${doc.id}`}
|
||||
@@ -19,15 +25,21 @@ export const DocSearchItem = ({ doc }: DocSearchItemProps) => {
|
||||
left={
|
||||
<Box $direction="row" $align="center" $gap="10px" $width="100%">
|
||||
<Box $flex={isDesktop ? 9 : 1}>
|
||||
<SimpleDocItem doc={doc} showAccesses />
|
||||
<SimpleDocItem
|
||||
doc={doc}
|
||||
showDate
|
||||
isPinned={doc.is_favorite}
|
||||
breadcrumb={filter === 'all' ? doc.parent?.title : undefined}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
right={
|
||||
<Icon
|
||||
iconName="keyboard_return"
|
||||
$padding={{ horizontal: '3xs' }}
|
||||
$theme="brand"
|
||||
$variation="secondary"
|
||||
icon={<ArrowIcon width={16} height={16} aria-hidden="true" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+100
-64
@@ -2,7 +2,7 @@ import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
@@ -10,21 +10,29 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Box, ButtonCloseModal, Text } from '@/components';
|
||||
import { QuickSearch } from '@/components/quick-search';
|
||||
import { Doc, useDocUtils } from '@/docs/doc-management';
|
||||
import {
|
||||
DocSearchFilters,
|
||||
DocSearchFiltersValues,
|
||||
DocSearchTarget,
|
||||
} from '@/docs/doc-search';
|
||||
import { useAuth } from '@/features/auth/hooks/useAuth';
|
||||
import { useFocusStore, useResponsiveStore } from '@/stores';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocSearch } from '../api/useSearchDocs';
|
||||
import EmptySearchIcon from '../assets/illustration-docs-empty.png';
|
||||
import { useDocSearchFilterStore } from '../stores/useDocSearchFilterStore';
|
||||
import { DocSearchFilterTypes } from '../types';
|
||||
|
||||
import { DocSearchContent } from './DocSearchContent';
|
||||
import { DocSearchFilters } from './DocSearchFilters';
|
||||
|
||||
const ModalStyle = createGlobalStyle`
|
||||
.c__modal__scroller {
|
||||
overflow: inherit ;
|
||||
|
||||
&:has(.quick-search-container) > div.c__modal__title {
|
||||
padding-top: var(--c--globals--spacings--sm);
|
||||
padding-bottom: var(--c--globals--spacings--xs);
|
||||
padding-inline: var(--c--globals--spacings--base);
|
||||
}
|
||||
.quick-search-input {
|
||||
padding: var(--c--globals--spacings--xxs) var(--c--globals--spacings--base);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -32,7 +40,7 @@ type DocSearchModalGlobalProps = {
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
showFilters?: boolean;
|
||||
defaultFilters?: DocSearchFiltersValues;
|
||||
defaultFilters: DocSearchFilterTypes;
|
||||
parentPath?: string; // If defined, the search will be limited to the children of the document with the given path
|
||||
};
|
||||
|
||||
@@ -45,24 +53,36 @@ const DocSearchModalGlobal = ({
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [results, setResults] = useState<Doc[]>([]);
|
||||
const restoreFocus = useFocusStore((state) => state.restoreFocus);
|
||||
const router = useRouter();
|
||||
const [search, setSearch] = useState('');
|
||||
const [filters, setFilters] = useState<DocSearchFiltersValues>(
|
||||
defaultFilters ?? {},
|
||||
);
|
||||
const { isLargeScreen } = useResponsiveStore();
|
||||
const handleInputSearch = useDebouncedCallback(setSearch, 300);
|
||||
const { filter, setFilter } = useDocSearchFilterStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!search) {
|
||||
setResults([]);
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilter(defaultFilters);
|
||||
}, [defaultFilters, setFilter]);
|
||||
|
||||
const handleSelect = (doc: Doc) => {
|
||||
void router.push(`/docs/${doc.id}`);
|
||||
modalProps.onClose?.();
|
||||
};
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setFilters({});
|
||||
restoreFocus();
|
||||
};
|
||||
/**
|
||||
* When searching within the current document, we only want to show sub-documents
|
||||
* Otherwise, we show all documents in the search results
|
||||
*/
|
||||
const filterResults = useCallback(
|
||||
(doc: DocSearch) =>
|
||||
(filter === 'current' && !!doc.parent) || filter === 'all' || !filter,
|
||||
[filter],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -71,51 +91,55 @@ const DocSearchModalGlobal = ({
|
||||
size={isLargeScreen ? ModalSize.LARGE : ModalSize.FULL}
|
||||
hideCloseButton
|
||||
aria-describedby="doc-search-modal-title"
|
||||
title={
|
||||
<>
|
||||
<Text as="h2" $margin="0" $size="s" $align="flex-start">
|
||||
{t('Search for a document')}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 4px; right: 4px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the search modal')}
|
||||
onClick={modalProps.onClose}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ModalStyle />
|
||||
<Box
|
||||
aria-label={t('Search modal')}
|
||||
$direction="column"
|
||||
$justify="space-between"
|
||||
className="--docs--doc-search-modal"
|
||||
$padding={{ vertical: 'base' }}
|
||||
$padding={{ bottom: 'base' }}
|
||||
aria-label={t('Search modal')}
|
||||
>
|
||||
<Text
|
||||
as="h1"
|
||||
$margin="0"
|
||||
id="doc-search-modal-title"
|
||||
className="sr-only"
|
||||
>
|
||||
{t('Search docs')}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 4px; right: 4px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the search modal')}
|
||||
onClick={modalProps.onClose}
|
||||
size="small"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
/>
|
||||
</Box>
|
||||
<QuickSearch
|
||||
label={t('Search documents')}
|
||||
placeholder={t('Type the name of a document')}
|
||||
loading={loading}
|
||||
onFilter={handleInputSearch}
|
||||
beforeList={
|
||||
showFilters ? (
|
||||
<Box $padding={{ horizontal: '10px' }}>
|
||||
<DocSearchFilters
|
||||
values={filters}
|
||||
onValuesChange={setFilters}
|
||||
onReset={handleResetFilters}
|
||||
<Box
|
||||
$margin={{ top: 'sm', horizontal: 'base' }}
|
||||
$justify="space-between"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
role="group"
|
||||
aria-label={t('Search results controls')}
|
||||
>
|
||||
<Text $color="textSecondary" $weight="700">
|
||||
<DocSearchStateText
|
||||
hasResults={results.length > 0}
|
||||
filter={filter}
|
||||
isSearching={!!search}
|
||||
/>
|
||||
</Box>
|
||||
) : undefined
|
||||
</Text>
|
||||
{showFilters && <DocSearchFilters />}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
$padding={{ horizontal: '10px', vertical: 'base' }}
|
||||
$padding={{ horizontal: 'sm', bottom: 'base' }}
|
||||
$height={isLargeScreen ? '500px' : 'calc(100vh - 68px - 1rem)'}
|
||||
>
|
||||
{search.length === 0 && (
|
||||
@@ -130,21 +154,12 @@ const DocSearchModalGlobal = ({
|
||||
)}
|
||||
{search && (
|
||||
<DocSearchContent
|
||||
groupName={results.length ? t('Select a document') : ''}
|
||||
filterResults={filterResults}
|
||||
search={search}
|
||||
onSelect={handleSelect}
|
||||
onResults={setResults}
|
||||
onLoadingChange={setLoading}
|
||||
target={
|
||||
filters.target === DocSearchTarget.CURRENT
|
||||
? DocSearchTarget.CURRENT
|
||||
: DocSearchTarget.ALL
|
||||
}
|
||||
parentPath={
|
||||
filters.target === DocSearchTarget.CURRENT
|
||||
? parentPath
|
||||
: undefined
|
||||
}
|
||||
parentPath={filter === 'current' ? parentPath : undefined}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@@ -167,18 +182,11 @@ const DocSearchModalDetail = ({
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const { authenticated } = useAuth();
|
||||
|
||||
let defaultFilters = DocSearchTarget.ALL;
|
||||
let showFilters = false;
|
||||
if (isWithChildren) {
|
||||
defaultFilters = DocSearchTarget.CURRENT;
|
||||
showFilters = authenticated;
|
||||
}
|
||||
|
||||
return (
|
||||
<DocSearchModalGlobal
|
||||
{...modalProps}
|
||||
showFilters={showFilters}
|
||||
defaultFilters={{ target: defaultFilters }}
|
||||
showFilters={isWithChildren && authenticated}
|
||||
defaultFilters={isWithChildren ? 'current' : 'all'}
|
||||
parentPath={treeContext?.root?.path}
|
||||
/>
|
||||
);
|
||||
@@ -195,3 +203,31 @@ export const DocSearchModal = ({ doc, ...modalProps }: DocSearchModalProps) => {
|
||||
|
||||
return <DocSearchModalGlobal {...modalProps} />;
|
||||
};
|
||||
|
||||
interface DocSearchStateTextProps {
|
||||
hasResults: boolean;
|
||||
filter: DocSearchFilterTypes;
|
||||
isSearching: boolean;
|
||||
}
|
||||
|
||||
const DocSearchStateText = ({
|
||||
hasResults,
|
||||
filter,
|
||||
isSearching,
|
||||
}: DocSearchStateTextProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (hasResults && filter === 'all') {
|
||||
return t('Select a document');
|
||||
}
|
||||
|
||||
if (hasResults && filter === 'current') {
|
||||
return t('Select a sub-document');
|
||||
}
|
||||
|
||||
if (isSearching && !hasResults) {
|
||||
return t('No documents found');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { DocSearchFilterTypes } from '../types';
|
||||
|
||||
export interface UseDocSearchFilterStore {
|
||||
filter: DocSearchFilterTypes;
|
||||
setFilter: (filter: DocSearchFilterTypes) => void;
|
||||
}
|
||||
|
||||
const defaultState: Pick<UseDocSearchFilterStore, 'filter'> = {
|
||||
filter: 'all',
|
||||
};
|
||||
|
||||
export const useDocSearchFilterStore = create<UseDocSearchFilterStore>(
|
||||
(set) => ({
|
||||
filter: defaultState.filter,
|
||||
setFilter: (filter) => {
|
||||
set({ filter });
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export type DocSearchFilterTypes = 'all' | 'current';
|
||||
@@ -380,7 +380,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
tabIndex={-1} // avoid double tabstop
|
||||
>
|
||||
<Box $direction="row" $align="center" $width="100%">
|
||||
<SimpleDocItem doc={treeContext.root} showAccesses={true} />
|
||||
<SimpleDocItem doc={treeContext.root} showDate={true} />
|
||||
<DocTreeItemActions
|
||||
doc={treeContext.root}
|
||||
onCreateSuccess={(createdDoc) => {
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
|
||||
`}
|
||||
aria-label={`${doc.title}, ${t('Updated')} ${DateTime.fromISO(doc.updated_at).toRelative()}`}
|
||||
>
|
||||
<SimpleDocItem showAccesses doc={doc} />
|
||||
<SimpleDocItem showDate doc={doc} />
|
||||
</StyledLink>
|
||||
<Box className="pinned-actions" $align="center">
|
||||
<DocsGridActions doc={doc} />
|
||||
|
||||
Reference in New Issue
Block a user