diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index fa0089bc27..dcd8e8fb7f 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -151,11 +151,8 @@ export function Header(props: { encodeClientSiteSections(context, sections).current : undefined } - spaceTitle={siteSpace.title} - siteSpaceId={siteSpace.id} - siteSpaceIds={siteSpaces - .filter((s) => s.space.language === siteSpace.space.language) - .map((s) => s.id)} + siteSpace={siteSpace} + siteSpaces={siteSpaces} viewport={!withTopHeader ? 'mobile' : undefined} /> diff --git a/packages/gitbook/src/components/Search/SearchContainer.tsx b/packages/gitbook/src/components/Search/SearchContainer.tsx index f2f29f7e64..5c02a96c57 100644 --- a/packages/gitbook/src/components/Search/SearchContainer.tsx +++ b/packages/gitbook/src/components/Search/SearchContainer.tsx @@ -1,7 +1,7 @@ 'use client'; import { t, useLanguage } from '@/intl/client'; -import { CustomizationSearchStyle, type SiteSection } from '@gitbook/api'; +import { CustomizationSearchStyle, type SiteSection, type SiteSpace } from '@gitbook/api'; import { useRouter } from 'next/navigation'; import React, { useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -21,14 +21,11 @@ import { useSearchResults } from './useSearchResults'; import { useSearchResultsCursor } from './useSearchResultsCursor'; interface SearchContainerProps { - /** The current site space id. */ - siteSpaceId: string; + /** The current site space. */ + siteSpace: SiteSpace; - /** The title of the current space. */ - spaceTitle: string; - - /** The ids of all spaces in the current section. */ - siteSpaceIds: string[]; + /** All site spaces in the current section. */ + siteSpaces: ReadonlyArray; /** Whether there are sections on the site. */ withSections: boolean; @@ -50,20 +47,17 @@ interface SearchContainerProps { /** * Client component to render the search input and results. */ -export function SearchContainer(props: SearchContainerProps) { - const { - siteSpaceId, - spaceTitle, - section, - withVariants, - withSiteVariants, - withSections, - style, - className, - viewport, - siteSpaceIds, - } = props; - +export function SearchContainer({ + siteSpace, + section, + withVariants, + withSiteVariants, + withSections, + style, + className, + viewport, + siteSpaces, +}: SearchContainerProps) { const { assistants } = useAI(); const [state, setSearchState] = useSearch(); @@ -181,10 +175,29 @@ export function SearchContainer(props: SearchContainerProps) { const visible = viewport === 'desktop' ? !isMobile : viewport === 'mobile' ? isMobile : true; const searchResultsId = `search-results-${React.useId()}`; + + // If searching all variants of the current section (the "extended" scope), + // filter by language if the language is set for both the current and the target site space. + const siteSpaceIds = React.useMemo( + () => + siteSpaces.reduce((acc: string[], ss) => { + if ( + !siteSpace.space.language || + !ss.space.language || + ss.space.language === siteSpace.space.language + ) { + acc.push(ss.id); + } + + return acc; + }, []), + [siteSpaces, siteSpace.space.language] + ); + const { results, fetching, error } = useSearchResults({ disabled: !(state?.query || withAI), query: normalizedQuery, - siteSpaceId, + siteSpaceId: siteSpace.id, siteSpaceIds, scope: state?.scope ?? 'default', withAI: withAI, @@ -233,7 +246,7 @@ export function SearchContainer(props: SearchContainerProps) {
diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index 755662f754..7aace884a8 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -66,53 +66,6 @@ export interface AskAnswerResult { sources: AskAnswerSource[]; } -/** - * Server action to search content in the entire site. - */ -export async function searchAllSiteContent(query: string): Promise { - return traceErrorOnly('Search.searchAllSiteContent', async () => { - const context = await getServerActionBaseContext(); - return searchSiteContent(context, { - query, - scope: { mode: 'all' }, - }); - }); -} - -/** - * Server action to search content in a space. - */ -export async function searchCurrentSiteSpaceContent( - query: string, - siteSpaceId: string -): Promise { - return traceErrorOnly('Search.searchSiteSpaceContent', async () => { - const context = await getServerActionBaseContext(); - - return await searchSiteContent(context, { - query, - scope: { mode: 'current', siteSpaceId }, - }); - }); -} - -/** - * Server action to search content in a specific space. - */ -export async function searchSpecificSiteSpaceContent( - query: string, - siteSpaceIds: string[] -): Promise { - return traceErrorOnly('Search.searchSiteSpaceContent', async () => { - const context = await getServerActionBaseContext(); - - return await searchSiteContent(context, { - query, - scope: { mode: 'specific', siteSpaceIds }, - }); - }); -} - /** * Server action to ask a question in a space. */ @@ -245,69 +198,72 @@ export async function streamRecommendedQuestions(args: { siteSpaceId?: string }) /** * Search for content in a site by scoping the search to all content, a specific spaces or current space. */ -async function searchSiteContent( - context: GitBookBaseContext, - args: { - query: string; - scope: - | { mode: 'all' } - | { mode: 'current'; siteSpaceId: string } - | { mode: 'specific'; siteSpaceIds: string[] }; - } -): Promise { - const { dataFetcher } = context; - const siteURLData = await getSiteURLDataFromMiddleware(); - - const { scope, query } = args; - - if (query.length <= 1) { - return []; - } - - const [searchResults, { structure }] = await Promise.all([ - throwIfDataError( - dataFetcher.searchSiteContent({ - organizationId: siteURLData.organization, - siteId: siteURLData.site, - query, - scope, - }) - ), - throwIfDataError( - dataFetcher.getPublishedContentSite({ - organizationId: siteURLData.organization, - siteId: siteURLData.site, - siteShareKey: siteURLData.shareKey, - }) - ), - ]); - - return ( - await Promise.all( - searchResults.map((spaceItem) => { - const found = findSiteSpaceBy( - structure, - (siteSpace) => siteSpace.space.id === spaceItem.id - ); - const siteSection = found?.siteSection; - const siteSectionGroup = found?.siteSectionGroup; - - return Promise.all( - spaceItem.pages.map((pageItem) => - transformSitePageResult(context, { - pageItem, - spaceItem, - siteSpace: found?.siteSpace, - space: found?.siteSpace.space, - spaceURL: found?.siteSpace.urls.published, - siteSection: siteSection ?? undefined, - siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined, - }) - ) - ); - }) - ) - ).flat(2); +export async function searchSiteContent({ + query, + ...scope +}: { + query: string; +} & ( + | { mode: 'all' } + | { mode: 'current'; siteSpaceId: string } + | { mode: 'specific'; siteSpaceIds: string[] } +)): Promise { + return traceErrorOnly(`Search.searchSiteContent.${scope.mode}`, async () => { + if (query.length <= 1) { + return []; + } + + const [context, { organization, site, shareKey }] = await Promise.all([ + getServerActionBaseContext(), + getSiteURLDataFromMiddleware(), + ]); + + const [searchResults, { structure }] = await Promise.all([ + throwIfDataError( + context.dataFetcher.searchSiteContent({ + organizationId: organization, + siteId: site, + query, + scope, + }) + ), + throwIfDataError( + context.dataFetcher.getPublishedContentSite({ + organizationId: organization, + siteId: site, + siteShareKey: shareKey, + }) + ), + ]); + + return ( + await Promise.all( + searchResults.map((spaceItem) => { + const found = findSiteSpaceBy( + structure, + (siteSpace) => siteSpace.space.id === spaceItem.id + ); + const siteSection = found?.siteSection; + const siteSectionGroup = found?.siteSectionGroup; + + return Promise.all( + spaceItem.pages.map((pageItem) => + transformSitePageResult(context, { + pageItem, + spaceItem, + siteSpace: found?.siteSpace, + space: found?.siteSpace.space, + spaceURL: found?.siteSpace.urls.published, + siteSection: siteSection ?? undefined, + siteSectionGroup: + (siteSectionGroup as SiteSectionGroup) ?? undefined, + }) + ) + ); + }) + ) + ).flat(2); + }); } async function transformAnswer( diff --git a/packages/gitbook/src/components/Search/useSearchResults.ts b/packages/gitbook/src/components/Search/useSearchResults.ts index 25880f123d..6d4ce819a4 100644 --- a/packages/gitbook/src/components/Search/useSearchResults.ts +++ b/packages/gitbook/src/components/Search/useSearchResults.ts @@ -5,13 +5,12 @@ import { assert } from 'ts-essentials'; import { type OrderedComputedResult, - searchAllSiteContent, - searchCurrentSiteSpaceContent, - searchSpecificSiteSpaceContent, + searchSiteContent, streamRecommendedQuestions, } from './server-actions'; import { type Assistant, useAI } from '@/components/AI'; +import assertNever from 'assert-never'; import { useTrackEvent } from '../Insights'; import { isQuestion } from './isQuestion'; import type { SearchScope } from './useSearch'; @@ -123,23 +122,26 @@ export function useSearchResults(props: { const timeout = setTimeout(async () => { try { const results = await (() => { - if (scope === 'all') { - // Search all content on the site - return searchAllSiteContent(query); + switch (scope) { + case 'all': + // Search all content on the site + return searchSiteContent({ query, mode: 'all' }); + case 'default': + // Search the current section's variant + matched/default variant for other sections + return searchSiteContent({ query, mode: 'current', siteSpaceId }); + case 'extended': + // Search all variants of the current section + return searchSiteContent({ query, mode: 'specific', siteSpaceIds }); + case 'current': + // Search only the current section's current variant + return searchSiteContent({ + query, + mode: 'specific', + siteSpaceIds: [siteSpaceId], + }); + default: + assertNever(scope); } - if (scope === 'default') { - // Search the current section's variant + matched/default variant for other sections - return searchCurrentSiteSpaceContent(query, siteSpaceId); - } - if (scope === 'extended') { - // Search all variants of the current section - return searchSpecificSiteSpaceContent(query, siteSpaceIds); - } - if (scope === 'current') { - // Search only the current section's current variant - return searchSpecificSiteSpaceContent(query, [siteSpaceId]); - } - throw new Error(`Unhandled search scope: ${scope}`); })(); if (cancelled) { diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 97ce952411..2f2f2870b8 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -170,8 +170,10 @@ export function SpaceLayout(props: SpaceLayoutProps) {
) } + // Displays the search button and/or the space dropdown in the ToC + // according to the header/variant settings. + // E.g if there is no header, the search button will be displayed in the ToC. innerHeader={ - // displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC. <> {!withTopHeader && (
@@ -187,15 +189,8 @@ export function SpaceLayout(props: SpaceLayoutProps) { } withSections={withSections} section={sections?.current} - spaceTitle={siteSpace.title} - siteSpaceId={siteSpace.id} - siteSpaceIds={siteSpaces - .filter( - (s) => - s.space.language === - siteSpace.space.language - ) - .map((s) => s.id)} + siteSpace={siteSpace} + siteSpaces={siteSpaces} className="max-lg:hidden" viewport="desktop" />