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()}`} > - +