Skip to content

Commit aaf8daf

Browse files
authored
Improve search UI (GitbookIO#2643)
1 parent 8af1abc commit aaf8daf

File tree

6 files changed

+67
-86
lines changed

6 files changed

+67
-86
lines changed

bun.lockb

112 Bytes
Binary file not shown.

packages/gitbook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"memoizee": "^0.4.15",
4444
"next": "14.2.15",
4545
"next-themes": "^0.2.1",
46-
"nuqs": "^1.17.4",
46+
"nuqs": "^2.2.3",
4747
"object-hash": "^3.0.0",
4848
"openapi-types": "^12.1.3",
4949
"p-map": "^7.0.0",

packages/gitbook/src/app/(site)/(content)/layout.tsx

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CustomizationThemeMode } from '@gitbook/api';
22
import { Metadata, Viewport } from 'next';
33
import { headers } from 'next/headers';
4+
import { NuqsAdapter } from 'nuqs/adapters/next/app';
45
import React from 'react';
56
import * as ReactDOM from 'react-dom';
67

@@ -55,46 +56,48 @@ export default async function ContentLayout(props: { children: React.ReactNode }
5556
});
5657

5758
return (
58-
<ClientContexts
59-
nonce={nonce}
60-
forcedTheme={
61-
getQueryStringTheme() ??
62-
(customization.themes.toggeable ? undefined : customization.themes.default)
63-
}
64-
>
65-
<SpaceLayout
66-
space={space}
67-
contentTarget={contentTarget}
68-
site={site}
69-
spaces={spaces}
70-
sections={sections}
71-
customization={customization}
72-
pages={pages}
73-
ancestors={ancestors}
74-
content={content}
59+
<NuqsAdapter>
60+
<ClientContexts
61+
nonce={nonce}
62+
forcedTheme={
63+
getQueryStringTheme() ??
64+
(customization.themes.toggeable ? undefined : customization.themes.default)
65+
}
7566
>
76-
{children}
77-
</SpaceLayout>
78-
79-
{scripts.length > 0 ? (
80-
<>
81-
<LoadIntegrations />
82-
{scripts.map(({ script }) => (
83-
<script key={script} async src={script} nonce={nonce} />
84-
))}
85-
</>
86-
) : null}
87-
88-
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
89-
<React.Suspense fallback={null}>
90-
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
91-
</React.Suspense>
92-
) : null}
93-
94-
<RocketLoaderDetector nonce={nonce} />
95-
96-
<AdminToolbar space={space} content={content} />
97-
</ClientContexts>
67+
<SpaceLayout
68+
space={space}
69+
contentTarget={contentTarget}
70+
site={site}
71+
spaces={spaces}
72+
sections={sections}
73+
customization={customization}
74+
pages={pages}
75+
ancestors={ancestors}
76+
content={content}
77+
>
78+
{children}
79+
</SpaceLayout>
80+
81+
{scripts.length > 0 ? (
82+
<>
83+
<LoadIntegrations />
84+
{scripts.map(({ script }) => (
85+
<script key={script} async src={script} nonce={nonce} />
86+
))}
87+
</>
88+
) : null}
89+
90+
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
91+
<React.Suspense fallback={null}>
92+
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
93+
</React.Suspense>
94+
) : null}
95+
96+
<RocketLoaderDetector nonce={nonce} />
97+
98+
<AdminToolbar space={space} content={content} />
99+
</ClientContexts>
100+
</NuqsAdapter>
98101
);
99102
}
100103

packages/gitbook/src/components/Search/SearchAskAnswer.tsx

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,42 +51,29 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
5151
React.useEffect(() => {
5252
let cancelled = false;
5353

54-
setState({
55-
type: 'loading',
56-
});
54+
setState({ type: 'loading' });
5755

5856
(async () => {
59-
const stream = iterateStreamResponse(
60-
streamAskQuestion(organizationId, siteId, siteSpaceId ?? null, query),
61-
);
57+
const response = streamAskQuestion(organizationId, siteId, siteSpaceId ?? null, query);
58+
const stream = iterateStreamResponse(response);
6259

63-
setSearchState((prev) =>
64-
prev
65-
? {
66-
...prev,
67-
ask: true,
68-
query,
69-
}
70-
: null,
71-
);
60+
// When we pass in "ask" mode, the query could still be updated by the client
61+
// we ensure that the query is up-to-date before starting the stream.
62+
setSearchState((prev) => (prev ? { ...prev, query, ask: true } : null));
7263

7364
for await (const chunk of stream) {
7465
if (cancelled) {
7566
return;
7667
}
77-
setState({
78-
type: 'answer',
79-
answer: chunk,
80-
});
68+
69+
setState({ type: 'answer', answer: chunk });
8170
}
82-
})().catch((error) => {
71+
})().catch(() => {
8372
if (cancelled) {
8473
return;
8574
}
8675

87-
setState({
88-
type: 'error',
89-
});
76+
setState({ type: 'error' });
9077
});
9178

9279
return () => {
@@ -96,7 +83,7 @@ export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: str
9683
cancelled = true;
9784
}
9885
};
99-
}, [organizationId, siteId, siteSpaceId, query]);
86+
}, [organizationId, siteId, siteSpaceId, query, setState, setSearchState]);
10087

10188
React.useEffect(() => {
10289
return () => {

packages/gitbook/src/components/Search/SearchModal.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { tcls } from '@/lib/tailwind';
1414
import { SearchAskAnswer, searchAskState } from './SearchAskAnswer';
1515
import { SearchResults, SearchResultsRef } from './SearchResults';
1616
import { SearchScopeToggle } from './SearchScopeToggle';
17-
import { SearchState, useSearch } from './useSearch';
17+
import { SearchState, UpdateSearchState, useSearch } from './useSearch';
1818
import { LoadingPane } from '../primitives/LoadingPane';
1919

2020
interface SearchModalProps {
@@ -55,10 +55,6 @@ export function SearchModal(props: SearchModalProps) {
5555
};
5656
}, [isSearchOpened]);
5757

58-
const onChangeQuery = (newQuery: SearchState) => {
59-
setSearchState(newQuery);
60-
};
61-
6258
const onClose = async (to?: string) => {
6359
await setSearchState(null);
6460
if (to) {
@@ -129,7 +125,7 @@ export function SearchModal(props: SearchModalProps) {
129125
<SearchModalBody
130126
{...props}
131127
state={state}
132-
onChangeQuery={onChangeQuery}
128+
setSearchState={setSearchState}
133129
onClose={onClose}
134130
/>
135131
</div>
@@ -142,7 +138,7 @@ export function SearchModal(props: SearchModalProps) {
142138
function SearchModalBody(
143139
props: SearchModalProps & {
144140
state: SearchState;
145-
onChangeQuery: (newQuery: SearchState) => void;
141+
setSearchState: UpdateSearchState;
146142
onClose: (to?: string) => void;
147143
},
148144
) {
@@ -154,7 +150,7 @@ function SearchModalBody(
154150
withAsk,
155151
isMultiVariants,
156152
state,
157-
onChangeQuery,
153+
setSearchState,
158154
onClose,
159155
} = props;
160156

@@ -193,7 +189,7 @@ function SearchModalBody(
193189
};
194190

195191
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
196-
onChangeQuery({
192+
setSearchState({
197193
ask: false, // When typing, we go back to the default search mode
198194
query: event.target.value,
199195
global: state.global,
@@ -312,11 +308,7 @@ function SearchModalBody(
312308
query={state.query}
313309
withAsk={withAsk}
314310
onSwitchToAsk={() => {
315-
onChangeQuery({
316-
ask: true,
317-
query: state.query,
318-
global: state.global,
319-
});
311+
setSearchState((state) => (state ? { ...state, ask: true } : null));
320312
}}
321313
></SearchResults>
322314
) : null}

packages/gitbook/src/components/Search/useSearch.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,17 @@ const keyMap = {
1616
global: parseAsBoolean,
1717
};
1818

19-
const options: UseQueryStatesOptions = {
20-
history: 'replace',
21-
};
19+
export type UpdateSearchState = (
20+
update: React.SetStateAction<SearchState | null>,
21+
) => Promise<URLSearchParams>;
2222

2323
/**
2424
* Hook to access the current search query and update it.
2525
*/
26-
export function useSearch(): [
27-
SearchState | null,
28-
(update: React.SetStateAction<SearchState | null>) => Promise<URLSearchParams>,
29-
] {
30-
const [rawState, setRawState] = useQueryStates(keyMap, options);
26+
export function useSearch(): [SearchState | null, UpdateSearchState] {
27+
const [rawState, setRawState] = useQueryStates(keyMap, {
28+
history: 'replace',
29+
});
3130

3231
const state = React.useMemo<SearchState | null>(() => {
3332
if (rawState === null || rawState.q === null) {

0 commit comments

Comments
 (0)