(frontend) handle sub-docs

handle filtering on sub-docs
This commit is contained in:
charles
2026-02-16 16:43:14 +01:00
parent 3fd29f7057
commit 2b0afead5b
6 changed files with 99 additions and 74 deletions
@@ -8,5 +8,5 @@ export * from './useDocs';
export * from './useDocsFavorite';
export * from './useDuplicateDoc';
export * from './useRestoreDoc';
export * from './useSubDocs';
export * from './useUpdateDoc';
export * from './useSearchDocs';
@@ -1,43 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
import { KEY_LIST_DOC } from '@/docs/doc-management';
import { Doc } from '../types';
export type DocsParams = {
q: string;
};
export const constructParams = (params: DocsParams): URLSearchParams => {
const searchParams = new URLSearchParams();
if (params.q.length > 0) {
searchParams.set('q', params.q);
} else {
searchParams.set('q', '*');
}
return searchParams;
};
export type DocsResponse = APIList<Doc>;
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
const searchParams = constructParams(params);
const response = await fetchAPI(
`documents/search/?${searchParams.toString()}`,
);
if (!response.ok) {
throw new APIError('Failed to get the docs', await errorCauses(response));
}
return response.json() as Promise<DocsResponse>;
};
export const useSearchDocs = (params: DocsParams) => {
return useQuery<DocsResponse, APIError, DocsResponse>({
queryKey: [KEY_LIST_DOC, 'search', params],
queryFn: () => getDocs(params),
});
};
@@ -0,0 +1,69 @@
import { useQuery } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI, useAPIInfiniteQuery } from '@/api';
import { DocSearchTarget } from '@/docs/doc-search';
import { DocsResponse, KEY_LIST_DOC } from './useDocs';
export type SearchDocsParams = {
page: number;
q: string;
target?: DocSearchTarget;
parentPath?: string;
};
const constructParams = ({
q,
page,
target,
parentPath,
}: SearchDocsParams): URLSearchParams => {
const searchParams = new URLSearchParams();
searchParams.set('q', q);
if (target === DocSearchTarget.CURRENT && parentPath) {
searchParams.set('path', parentPath);
}
if (page) {
searchParams.set('page', page.toString());
}
return searchParams;
};
const searchDocs = async ({
q,
page,
target,
parentPath,
}: SearchDocsParams): Promise<DocsResponse> => {
const searchParams = constructParams({ q, page, target, parentPath });
const response = await fetchAPI(
`documents/search/?${searchParams.toString()}`,
);
if (!response.ok) {
throw new APIError('Failed to get the docs', await errorCauses(response));
}
return response.json() as Promise<DocsResponse>;
};
export const useSearchDocs = (
{ q, page, target, parentPath }: SearchDocsParams,
queryConfig?: { enabled?: boolean },
) => {
return useQuery<DocsResponse, APIError, DocsResponse>({
queryKey: [KEY_LIST_DOC, 'search', { q, page, target, parentPath }],
queryFn: () => searchDocs({ q, page, target, parentPath }),
...queryConfig,
});
};
export const useInfiniteSearchDocs = (
params: SearchDocsParams,
queryConfig?: { enabled?: boolean },
) => {
return useAPIInfiniteQuery(KEY_LIST_DOC, searchDocs, params, queryConfig);
};
@@ -1,43 +1,55 @@
import { t } from 'i18next';
import { useEffect, useMemo } from 'react';
import { InView } from 'react-intersection-observer';
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
import { useSearchDocs } from '@/docs/doc-management/api/searchDocs';
import { useInfiniteSearchDocs } from '@/docs/doc-management/api/useSearchDocs';
import { Doc } from '../../doc-management';
import { DocSearchFiltersValues } from './DocSearchFilters';
import { DocSearchItem } from './DocSearchItem';
type DocSearchContentProps = {
search: string;
filters: DocSearchFiltersValues;
onSelect: (doc: Doc) => void;
onLoadingChange?: (loading: boolean) => void;
};
export const DocSearchContent = ({
search,
filters,
onSelect,
onLoadingChange,
}: DocSearchContentProps) => {
const { data, isFetching, isRefetching, isLoading } = useSearchDocs({
const {
data,
isFetching,
isRefetching,
isLoading,
fetchNextPage,
hasNextPage,
} = useInfiniteSearchDocs({
page: 1,
q: search,
...filters,
});
const loading = isFetching || isRefetching || isLoading;
const docsData: QuickSearchData<Doc> = useMemo(() => {
const docs = data?.results || [];
const docs = data?.pages.flatMap((page) => page.results) || [];
return {
groupName: docs.length > 0 ? t('Select a document') : '',
elements: search ? docs : [],
emptyString: t('No document found'),
endActions: hasNextPage
? [
{
content: <InView onChange={() => void fetchNextPage()} />,
},
]
: [],
};
}, [search, data?.results]);
}, [search, data?.pages, fetchNextPage, hasNextPage]);
useEffect(() => {
onLoadingChange?.(loading);
@@ -124,7 +124,6 @@ const DocSearchModalGlobal = ({
{target === DocSearchTarget.ALL && (
<DocSearchContent
search={search}
filters={filters}
onSelect={handleSelect}
onLoadingChange={setLoading}
/>
@@ -132,7 +131,6 @@ const DocSearchModalGlobal = ({
{isDocPage && target === DocSearchTarget.CURRENT && (
<DocSearchSubPageContent
search={search}
filters={filters}
onSelect={handleSelect}
onLoadingChange={setLoading}
renderElement={(doc) => <DocSearchItem doc={doc} />}
@@ -4,13 +4,12 @@ import React, { useEffect, useState } from 'react';
import { InView } from 'react-intersection-observer';
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
import { Doc, useInfiniteSubDocs } from '@/docs/doc-management';
import { DocSearchFiltersValues } from './DocSearchFilters';
import { Doc } from '@/docs/doc-management';
import { useInfiniteSearchDocs } from '@/docs/doc-management/api/useSearchDocs';
import { DocSearchTarget } from '@/docs/doc-search';
type DocSearchSubPageContentProps = {
search: string;
filters: DocSearchFiltersValues;
onSelect: (doc: Doc) => void;
onLoadingChange?: (loading: boolean) => void;
renderElement: (doc: Doc) => React.ReactNode;
@@ -18,7 +17,6 @@ type DocSearchSubPageContentProps = {
export const DocSearchSubPageContent = ({
search,
filters,
onSelect,
onLoadingChange,
renderElement,
@@ -32,17 +30,18 @@ export const DocSearchSubPageContent = ({
isLoading,
fetchNextPage: subDocsFetchNextPage,
hasNextPage: subDocsHasNextPage,
} = useInfiniteSubDocs(
} = useInfiniteSearchDocs(
{
q: search,
page: 1,
title: search,
...filters,
parent_id: treeContext?.root?.id ?? '',
target: DocSearchTarget.CURRENT,
parentPath: treeContext?.root?.path,
},
{
enabled: !!treeContext?.root?.id,
enabled: !!treeContext?.root?.path,
},
);
const [docsData, setDocsData] = useState<QuickSearchData<Doc>>({
groupName: '',
elements: [],
@@ -58,16 +57,6 @@ export const DocSearchSubPageContent = ({
const subDocs = subDocsData?.pages.flatMap((page) => page.results) || [];
if (treeContext?.root) {
const isRootTitleIncludeSearch = treeContext.root?.title
?.toLowerCase()
.includes(search.toLowerCase());
if (isRootTitleIncludeSearch) {
subDocs.unshift(treeContext.root);
}
}
setDocsData({
groupName: subDocs.length > 0 ? t('Select a doc') : '',
elements: search ? subDocs : [],