Simplify search query handling to in-memory storage only (#4419)

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Tomek
2026-07-22 09:54:50 +02:00
committed by GitHub
parent ae9367dafe
commit 94ef1769ec
9 changed files with 173 additions and 30 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Keep the last search or ask query visible after closing its surface and restore it when reopening search or an embed.
@@ -197,8 +197,9 @@ export function EmbeddableIframeTabs(props: {
active?: string;
baseURL: string;
siteTitle: string;
onNavigate?: (href: string) => void;
}) {
const { ref, active = 'assistant', baseURL, siteTitle } = props;
const { ref, active = 'assistant', baseURL, siteTitle, onNavigate } = props;
const actions = useEmbeddableConfiguration((state) => state.actions);
const tabs = useEmbeddableTabs();
@@ -261,6 +262,10 @@ export function EmbeddableIframeTabs(props: {
className="not-hydrated:animate-blur-in-slow [&_.button-leading-icon]:size-5"
iconOnly
onClick={() => {
if (tab.key !== active && onNavigate) {
onNavigate(tab.href);
return;
}
router.push(tab.href);
}}
tooltipProps={{
@@ -274,7 +279,8 @@ export function EmbeddableIframeTabs(props: {
) : null;
}
export function EmbeddableIframeCloseButton() {
export function EmbeddableIframeCloseButton(props: { onClose?: () => void }) {
const { onClose } = props;
const { closeButton } = useEmbeddableConfiguration();
if (!closeButton) {
@@ -291,6 +297,7 @@ export function EmbeddableIframeCloseButton() {
className="not-hydrated:animate-blur-in-slow [&_.button-leading-icon]:size-5"
iconOnly
onClick={() => {
onClose?.();
getChannel()?.send({ type: 'close' });
}}
tooltipProps={{
@@ -38,10 +38,13 @@ export function EmbeddableSearch(props: EmbeddableSearchProps) {
const tabsRef = React.useRef<HTMLDivElement>(null);
const {
askQuery,
abort,
close,
cursor,
error,
fetching,
onInputKeyDown,
onResultSelect,
query,
results,
resultsId,
@@ -51,7 +54,10 @@ export function EmbeddableSearch(props: EmbeddableSearchProps) {
showAsk,
withSearchAI,
scopeControl,
} = useSearchController({ ...searchProps, asEmbeddable: hasDocsTab });
} = useSearchController(
{ ...searchProps, asEmbeddable: hasDocsTab },
{ restoreLastQueryOnMount: true }
);
return (
<LinkContext value={linkContext}>
@@ -66,6 +72,8 @@ export function EmbeddableSearch(props: EmbeddableSearchProps) {
results={results}
resultsId={resultsId}
resultsRef={resultsRef}
onAskSelect={abort}
onResultSelect={onResultSelect}
showAsk={showAsk}
dataTestId="embed-search"
input={
@@ -97,9 +105,10 @@ export function EmbeddableSearch(props: EmbeddableSearchProps) {
active="search"
baseURL={baseURL}
siteTitle={siteTitle}
onNavigate={close}
/>
<EmbeddableIframeButtons />
<EmbeddableIframeCloseButton />
<EmbeddableIframeCloseButton onClose={close} />
</>
}
scopeControl={
@@ -50,6 +50,7 @@ export function SearchContainer({
error,
fetching,
onInputKeyDown,
onResultSelect,
open,
query,
results,
@@ -169,7 +170,8 @@ export function SearchContainer({
results={results}
resultsId={resultsId}
resultsRef={resultsRef}
onResultSelect={abort}
onAskSelect={abort}
onResultSelect={onResultSelect}
showAsk={showAsk}
scopeControl={scopeControlNode}
fillHeight={usesSideSheet || shouldFillHeight}
@@ -29,7 +29,8 @@ export function SearchFrame(props: {
results: ResultType[];
resultsId: string;
resultsRef: React.Ref<SearchResultsRef>;
onResultSelect?: () => void;
onAskSelect?: () => void;
onResultSelect?: (result: ResultType) => void;
scopeControl?: React.ReactNode;
showAsk: boolean;
sidebar?: React.ReactNode;
@@ -50,6 +51,7 @@ export function SearchFrame(props: {
results,
resultsId,
resultsRef,
onAskSelect,
onResultSelect,
scopeControl,
showAsk,
@@ -126,7 +128,7 @@ export function SearchFrame(props: {
assistant={assistant}
active={cursor === results.length + index}
withShortcut={assistant === assistants[0]}
onSelect={onResultSelect}
onSelect={onAskSelect}
/>
))
: null}
@@ -50,7 +50,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
fetching: boolean;
cursor: number | null;
error: boolean;
onResultSelect?: () => void;
onResultSelect?: (result: ResultType) => void;
},
ref: React.Ref<SearchResultsRef>
) {
@@ -211,7 +211,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
addRecentSearchQuery(siteSpaceId, query, 'search');
}
onResultSelect?.();
onResultSelect?.(item);
};
const resultItemProps = {
'aria-posinset': index + 1,
@@ -0,0 +1,52 @@
'use client';
import React from 'react';
type LastSearchQueryBySiteSpace = Record<string, string>;
const listeners = new Set<() => void>();
let globalLastSearchQuery: LastSearchQueryBySiteSpace = {};
function emitChange() {
listeners.forEach((listener) => listener());
}
export function getLastSearchQuery(siteSpaceId: string): string | null {
return globalLastSearchQuery[siteSpaceId] ?? null;
}
export function setLastSearchQuery(siteSpaceId: string, query: string | null): void {
if (!siteSpaceId) {
return;
}
const normalizedQuery = query?.trim() || null;
const nextState = { ...globalLastSearchQuery };
if (normalizedQuery) {
nextState[siteSpaceId] = normalizedQuery;
} else {
delete nextState[siteSpaceId];
}
globalLastSearchQuery = nextState;
emitChange();
}
export function clearLastSearchQuery(siteSpaceId: string): void {
setLastSearchQuery(siteSpaceId, null);
}
function subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function useLastSearchQuery(siteSpaceId: string): string | null {
return React.useSyncExternalStore(
subscribe,
() => globalLastSearchQuery[siteSpaceId] ?? null,
() => null
);
}
@@ -61,6 +61,12 @@ function normalizeRawState(values: Values<typeof keyMap>) {
return values;
}
export function shouldKeepSearchState(
values: Pick<Values<typeof keyMap>, 'q' | 'ask' | 'scope'>
): boolean {
return values.q !== null || values.ask !== null || values.scope !== 'default';
}
export function SearchContextProvider(props: React.PropsWithChildren): React.ReactElement {
const { children } = props;
@@ -74,7 +80,7 @@ export function SearchContextProvider(props: React.PropsWithChildren): React.Rea
const state = React.useMemo<SearchState | null>(() => {
const normalized = normalizeRawState(rawState);
if (normalized.q === null && normalized.ask === null) {
if (!shouldKeepSearchState(normalized)) {
return null;
}
return {
@@ -7,10 +7,16 @@ import { useAI } from '../AI';
import { useTrackEvent } from '../Insights';
import { useBodyLoaded } from '../primitives';
import type { SearchResultsRef } from './SearchResults';
import {
clearLastSearchQuery,
getLastSearchQuery,
setLastSearchQuery,
useLastSearchQuery,
} from './last-query';
import { addRecentSearchQuery } from './recent-queries';
import type { SearchBaseProps } from './search-props';
import { useSearchState, useSetSearchState } from './useSearch';
import { useSearchResults } from './useSearchResults';
import { type ResultType, useSearchResults } from './useSearchResults';
import { useSearchResultsCursor } from './useSearchResultsCursor';
function useInitialAskBootstrap(props: {
@@ -109,7 +115,10 @@ function useSearchKeyboardNavigation(props: {
};
}
export function useSearchController(props: SearchBaseProps) {
export function useSearchController(
props: SearchBaseProps,
options: { restoreLastQueryOnMount?: boolean } = {}
) {
const {
asEmbeddable,
siteSpace,
@@ -130,6 +139,32 @@ export function useSearchController(props: SearchBaseProps) {
const resultsRef = React.useRef<SearchResultsRef>(null);
const isLoaded = useBodyLoaded();
const restoredLastQueryForSiteSpaceRef = React.useRef<string | null>(null);
React.useEffect(() => {
if (
!options.restoreLastQueryOnMount ||
restoredLastQueryForSiteSpaceRef.current === siteSpace.id
) {
return;
}
restoredLastQueryForSiteSpaceRef.current = siteSpace.id;
const restoredQuery = getLastSearchQuery(siteSpace.id);
if (!restoredQuery) {
return;
}
void setSearchState(
(prev) =>
prev ?? {
ask: null,
query: restoredQuery,
scope: 'default',
open: true,
}
);
}, [options.restoreLastQueryOnMount, setSearchState, siteSpace.id]);
const withAI = assistants.length > 0;
const withSearchAI = assistants.filter((assistant) => assistant.mode === 'search').length > 0;
@@ -141,38 +176,46 @@ export function useSearchController(props: SearchBaseProps) {
const onClose = React.useCallback(
async (to?: string) => {
setSearchState((prev) =>
prev
? {
...prev,
open: false,
query: prev.query === '' ? null : prev.query,
}
: null
);
setSearchState((prev) => {
if (!prev) return null;
if (prev.query !== null) {
setLastSearchQuery(siteSpace.id, prev.query);
}
return { ...prev, open: false, query: null };
});
if (to) {
router.push(to);
}
},
[setSearchState, router]
[setSearchState, router, siteSpace.id]
);
const onOpen = React.useCallback(() => {
if (state?.open) {
return;
}
setSearchState((prev) => ({
ask: withAI ? (prev?.ask ?? null) : null,
scope: prev?.scope ?? 'default',
query: prev?.query ?? (withSearchAI || !withAI ? prev?.ask : null) ?? '',
open: true,
}));
setSearchState((prev) => {
const query =
prev?.query ??
getLastSearchQuery(siteSpace.id) ??
(withSearchAI || !withAI ? prev?.ask : null) ??
'';
return {
ask: withAI ? (prev?.ask ?? null) : null,
scope: prev?.scope ?? 'default',
query,
open: true,
};
});
trackEvent({
type: 'search_open',
});
}, [state?.open, setSearchState, trackEvent, withAI, withSearchAI]);
}, [state?.open, setSearchState, siteSpace.id, trackEvent, withAI, withSearchAI]);
const setQuery = React.useCallback(
(value: string) => {
@@ -212,9 +255,25 @@ export function useSearchController(props: SearchBaseProps) {
withSections,
});
const searchValue = state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? '';
const lastSearchQuery = useLastSearchQuery(siteSpace.id);
const searchValue =
state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? lastSearchQuery ?? '';
const searchResultsId = `search-results-${React.useId()}`;
const onResultSelect = React.useCallback(
(result: ResultType) => {
clearLastSearchQuery(siteSpace.id);
abort();
if (result.type !== 'recommended-question') {
void setSearchState((prev) =>
prev ? { ...prev, query: null, open: false } : null
);
}
},
[abort, setSearchState, siteSpace.id]
);
const askInAssistant = React.useCallback(
(assistantIndex = 0) => {
const assistant = assistants[assistantIndex];
@@ -260,6 +319,7 @@ export function useSearchController(props: SearchBaseProps) {
abort,
open: onOpen,
close: onClose,
onResultSelect,
query: normalizedQuery,
results,
resultsId: searchResultsId,