diff --git a/.changeset/lovely-lies-complain.md b/.changeset/lovely-lies-complain.md new file mode 100644 index 000000000..c8df9e3d5 --- /dev/null +++ b/.changeset/lovely-lies-complain.md @@ -0,0 +1,5 @@ +--- +'gitbook': minor +--- + +Add support for searching results in a sections site diff --git a/bun.lockb b/bun.lockb index ee93c9e05..61c39c34a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 8a095c00f..3c4f61568 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static" }, "dependencies": { - "@gitbook/api": "^0.71.0", + "@gitbook/api": "^0.72.0", "@gitbook/cache-do": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", "@gitbook/icons": "workspace:*", diff --git a/packages/gitbook/src/components/Search/SearchResults.tsx b/packages/gitbook/src/components/Search/SearchResults.tsx index 023f109df..edfd13f69 100644 --- a/packages/gitbook/src/components/Search/SearchResults.tsx +++ b/packages/gitbook/src/components/Search/SearchResults.tsx @@ -12,8 +12,8 @@ import { SearchSectionResultItem } from './SearchSectionResultItem'; import { getRecommendedQuestions, OrderedComputedResult, - searchCurrentSpaceContent, - searchSiteContent, + searchSiteSpaceContent, + searchAllSiteContent, } from './server-actions'; import { Loading } from '../primitives'; @@ -95,8 +95,8 @@ export const SearchResults = React.forwardRef(function SearchResults( setCursor(null); const fetchedResults = await (global - ? searchSiteContent({ query, pointer }) - : searchCurrentSpaceContent(query, pointer, revisionId)); + ? searchAllSiteContent(query, pointer) + : searchSiteSpaceContent(query, pointer, revisionId)); setResults(withAsk ? withQuestionResult(fetchedResults, query) : fetchedResults); }, 350); diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index cf60df8ac..efc626b98 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -2,6 +2,7 @@ import { RevisionPage, SearchAIAnswer, SearchPageResult, SiteSpace, Space } from '@gitbook/api'; import * as React from 'react'; +import { assert } from 'ts-essentials'; import { streamResponse } from '@/lib/actions'; import * as api from '@/lib/api'; @@ -45,47 +46,61 @@ export interface AskAnswerResult { } /** - * Search for content in the entire site. + * Search for content in a site by scoping the search to all content, a specific spaces or current space. */ -export async function searchSiteContent(args: { +async function searchSiteContent(args: { pointer: api.SiteContentPointer; query: string; - siteSpaceIds?: string[]; + scope: + | { mode: 'all' } + | { mode: 'current'; siteSpaceId: string } + | { mode: 'specific'; siteSpaceIds: string[] }; cacheBust?: string; }): Promise { - const { pointer, siteSpaceIds, query, cacheBust } = args; + const { pointer, scope, query, cacheBust } = args; if (query.length <= 1) { return []; } - if (siteSpaceIds?.length === 0) { - // if we have no siteSpaces to search in then we won't find anything. skip the call. - return []; - } + const needsStructure = + scope.mode === 'all' || + scope.mode === 'current' || + (scope.mode === 'specific' && scope.siteSpaceIds.length > 1); - const [searchResults, allSiteSpaces] = await Promise.all([ - api.searchSiteContent( - pointer.organizationId, - pointer.siteId, - query, - siteSpaceIds, - cacheBust, - ), - siteSpaceIds - ? null - : api.getSiteSpaces({ + const [searchResults, siteStructure] = await Promise.all([ + api.searchSiteContent(pointer.organizationId, pointer.siteId, query, scope, cacheBust), + needsStructure + ? api.getSiteStructure({ organizationId: pointer.organizationId, siteId: pointer.siteId, siteShareKey: pointer.siteShareKey, - }), + }) + : null, ]); - if (!siteSpaceIds) { + const siteSpaces = siteStructure + ? siteStructure.type === 'siteSpaces' + ? siteStructure.structure + : siteStructure.structure.reduce((prev, section) => { + const sectionSiteSpaces = section.siteSpaces.map((siteSpace) => ({ + ...siteSpace, + space: { + ...siteSpace.space, + title: section.title + ' › ' + siteSpace.space.title, + }, + })); + + prev.push(...sectionSiteSpaces); + return prev; + }, []) + : null; + + if (siteSpaces) { // We are searching all of this Site's content return searchResults.items .map((spaceItem) => { - const siteSpace = allSiteSpaces?.find( + const siteSpace = siteSpaces.find( (siteSpace) => siteSpace.space.id === spaceItem.id, ); @@ -102,21 +117,39 @@ export async function searchSiteContent(args: { } /** - * Server action to search content in the current space + * Server action to search content in the entire site. */ -export async function searchCurrentSpaceContent( +export async function searchAllSiteContent( + query: string, + pointer: api.SiteContentPointer, +): Promise { + return await searchSiteContent({ + pointer, + query, + scope: { mode: 'all' }, + }); +} + +/** + * Server action to search content in a space. + */ +export async function searchSiteSpaceContent( query: string, pointer: api.SiteContentPointer, revisionId: string, ): Promise { - const siteSpaceIds = pointer.siteSpaceId ? [pointer.siteSpaceId] : []; // if we don't have a siteSpaceID search all content + const siteSpaceId = pointer.siteSpaceId; + assert(siteSpaceId, 'Expected siteSpaceId for searchSiteSpaceContent'); - // This is a site so use a different function which we can eventually call directly - // We also want to break cache for this specific space if the revisionId is different so use it as a cache busting key return await searchSiteContent({ pointer, - siteSpaceIds, query, + // If we have a siteSectionId that means its a sections site use `current` mode + // which searches in the current space + all default spaces of sections + scope: pointer.siteSectionId + ? { mode: 'current', siteSpaceId } + : { mode: 'specific', siteSpaceIds: [siteSpaceId] }, + // We want to break cache for this specific space if the revisionId is different so use it as a cache busting key cacheBust: revisionId, }); } diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index 3dcb62bd3..6b388631d 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -1058,7 +1058,10 @@ export const searchSiteContent = cache({ organizationId: string, siteId: string, query: string, - siteSpaceIds?: string[], + scope: + | { mode: 'all' } + | { mode: 'current'; siteSpaceId: string } + | { mode: 'specific'; siteSpaceIds: string[] }, /** A cache bust param to avoid revalidating lot of cache entries by tags */ cacheBust?: string, options?: CacheFunctionOptions, @@ -1068,11 +1071,7 @@ export const searchSiteContent = cache({ siteId, { query, - ...(siteSpaceIds && siteSpaceIds.length > 0 - ? { siteSpaceIds } - : { - mode: 'all', - }), + ...scope, }, undefined, { diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index 41d1e750e..62f2ffafa 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "^0.66.0", + "@gitbook/api": "^0.72.0", "assert-never": "^1.2.1" }, "peerDependencies": {