From 359fb4260560e36dc82a787b6432af73f570e5cd Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 5 Jun 2026 11:04:33 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20top=20parent=20on?= =?UTF-8?q?=20sub=20docs=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 1 + .../components/quick-search/QuickSearch.tsx | 2 +- .../quick-search/QuickSearchStyle.tsx | 1 + .../features/docs/doc-management/api/index.ts | 1 - .../components/SimpleDocItem.tsx | 58 +++++-- .../api/useSearchDocs.tsx | 31 ++-- .../components/DocSearchButtonModal.tsx | 1 + .../components/DocSearchContent.tsx | 25 ++- .../components/DocSearchFilters.tsx | 99 +++++------ .../doc-search/components/DocSearchItem.tsx | 20 ++- .../doc-search/components/DocSearchModal.tsx | 164 +++++++++++------- .../stores/useDocSearchFilterStore.tsx | 21 +++ .../src/features/docs/doc-search/types.tsx | 1 + .../docs/doc-tree/components/DocTree.tsx | 2 +- .../components/LeftPanelFavoriteItem.tsx | 2 +- 15 files changed, 255 insertions(+), 174 deletions(-) rename src/frontend/apps/impress/src/features/docs/{doc-management => doc-search}/api/useSearchDocs.tsx (67%) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-search/stores/useDocSearchFilterStore.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-search/types.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f796ed661..98ba680c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx index a85b45e0d..4fc5f58a9 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx @@ -14,7 +14,7 @@ export type QuickSearchAction = { }; export type QuickSearchData = { - groupName: string; + groupName?: string; groupKey?: string; elements: T[]; emptyString?: string; diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx index ffe86e646..5c46596ec 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx @@ -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; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts index e710a364d..d0bf67148 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts @@ -10,4 +10,3 @@ export * from './useDuplicateDoc'; export * from './useMoveDoc'; export * from './useRestoreDoc'; export * from './useUpdateDoc'; -export * from './useSearchDocs'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx index da427069a..34653f57c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx @@ -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 = ({ /> )} - + {docTitle} - {(!isDesktop || showAccesses) && ( - diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useSearchDocs.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/api/useSearchDocs.tsx similarity index 67% rename from src/frontend/apps/impress/src/features/docs/doc-management/api/useSearchDocs.tsx rename to src/frontend/apps/impress/src/features/docs/doc-search/api/useSearchDocs.tsx index 9ea2e4674..54339e493 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useSearchDocs.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/api/useSearchDocs.tsx @@ -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; + const searchDocs = async ({ q, page, - target, + filter, parentPath, -}: SearchDocsParams): Promise> => { - const searchParams = constructParams({ q, page, target, parentPath }); +}: SearchDocsParams): Promise => { + 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>; + return response.json() as Promise; }; export const KEY_LIST_SEARCH_DOC = 'search-docs'; export const useSearchDocs = ( - { q, page, target, parentPath }: SearchDocsParams, + param: SearchDocsParams, queryConfig?: { enabled?: boolean }, ) => { - return useQuery, APIError, APIList>({ - queryKey: [KEY_LIST_SEARCH_DOC, 'search', { q, page, target, parentPath }], - queryFn: () => searchDocs({ q, page, target, parentPath }), + return useQuery({ + queryKey: [KEY_LIST_SEARCH_DOC, param], + queryFn: () => searchDocs(param), ...queryConfig, }); }; 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 index 02d95052f..6fcb612e6 100644 --- 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 @@ -58,6 +58,7 @@ export const DocSearchButtonModal = () => { onClose={closeSearchModal} isOpen={isSearchModalOpen} doc={currentDoc} + defaultFilters="all" /> )} diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx index 64a377bca..f9848d6ed 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx @@ -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>({ + const [docsData, setDocsData] = useState>({ groupName: '', groupKey: 'docs', elements: [], @@ -84,7 +82,6 @@ export const DocSearchContent = ({ groupName: groupName, groupKey: 'docs', elements, - emptyString: t('No document found'), endActions: hasNextPage ? [ { diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchFilters.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchFilters.tsx index a8a091118..7423f337f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchFilters.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchFilters.tsx @@ -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 + */ { + 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; + } + `} > - - handleTargetChange(DocSearchTarget.ALL), - }, - { - label: t('Current doc'), - value: DocSearchTarget.CURRENT, - callback: () => handleTargetChange(DocSearchTarget.CURRENT), - }, - ]} - /> - - {hasFilters && ( - - )} + setFilter(filter === 'all' ? 'current' : 'all')} + aria-label={t( + 'Toggle to search in all documents or only in current document', + )} + /> ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchItem.tsx index 8e5f8cdc7..1be332807 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchItem.tsx @@ -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 ( { left={ - + } right={ } > {search.length === 0 && ( @@ -130,21 +154,12 @@ const DocSearchModalGlobal = ({ )} {search && ( )} @@ -167,18 +182,11 @@ const DocSearchModalDetail = ({ const treeContext = useTreeContext(); const { authenticated } = useAuth(); - let defaultFilters = DocSearchTarget.ALL; - let showFilters = false; - if (isWithChildren) { - defaultFilters = DocSearchTarget.CURRENT; - showFilters = authenticated; - } - return ( ); @@ -195,3 +203,31 @@ export const DocSearchModal = ({ doc, ...modalProps }: DocSearchModalProps) => { return ; }; + +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; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/stores/useDocSearchFilterStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/stores/useDocSearchFilterStore.tsx new file mode 100644 index 000000000..52eafc05c --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-search/stores/useDocSearchFilterStore.tsx @@ -0,0 +1,21 @@ +import { create } from 'zustand'; + +import { DocSearchFilterTypes } from '../types'; + +export interface UseDocSearchFilterStore { + filter: DocSearchFilterTypes; + setFilter: (filter: DocSearchFilterTypes) => void; +} + +const defaultState: Pick = { + filter: 'all', +}; + +export const useDocSearchFilterStore = create( + (set) => ({ + filter: defaultState.filter, + setFilter: (filter) => { + set({ filter }); + }, + }), +); diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/types.tsx new file mode 100644 index 000000000..6e89cb594 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-search/types.tsx @@ -0,0 +1 @@ +export type DocSearchFilterTypes = 'all' | 'current'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx index 356898b77..5180d757d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx @@ -380,7 +380,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => { tabIndex={-1} // avoid double tabstop > - + { diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx index bc68a7853..254b23da3 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx @@ -55,7 +55,7 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => { `} aria-label={`${doc.title}, ${t('Updated')} ${DateTime.fromISO(doc.updated_at).toRelative()}`} > - +