Skip to content

Commit c531a6b

Browse files
authoredMar 8, 2024
refactor(core): use KeyValueStore for recent searches (#5872)
* refactor(core): create useStoredSearch hook * refactor: pass useStoredSearch to recentSearchesStore * refactor(core): convert createRecentSearchesStore to useRecentSearchesStore * fix(core): use default studio client options * fix: pass recent terms for more predictable UI behavior * refactor: remove recent searches from search state and invoke hook wherever needed * fix: remove typos, spaces
1 parent dd0794a commit c531a6b

File tree

11 files changed

+631
-559
lines changed

11 files changed

+631
-559
lines changed
 

‎packages/sanity/src/core/studio/components/navbar/search/components/recentSearches/RecentSearches.tsx

+10-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../../../../../../components'
1111
import {useTranslation} from '../../../../../../i18n'
1212
import {useSearchState} from '../../contexts/search/useSearchState'
13-
import {type RecentSearch} from '../../datastores/recentSearches'
13+
import {type RecentSearch, useRecentSearchesStore} from '../../datastores/recentSearches'
1414
import {Instructions} from '../Instructions'
1515
import {RecentSearchItem} from './item/RecentSearchItem'
1616

@@ -33,9 +33,14 @@ interface RecentSearchesProps {
3333
export function RecentSearches({inputElement}: RecentSearchesProps) {
3434
const {
3535
dispatch,
36-
recentSearchesStore,
37-
state: {filtersVisible, fullscreen, recentSearches},
36+
state: {filtersVisible, fullscreen},
3837
} = useSearchState()
38+
const recentSearchesStore = useRecentSearchesStore()
39+
const recentSearches = useMemo(
40+
() => recentSearchesStore?.getRecentSearches(),
41+
[recentSearchesStore],
42+
)
43+
3944
const commandListRef = useRef<CommandListHandle | null>(null)
4045

4146
const {t} = useTranslation()
@@ -46,11 +51,10 @@ export function RecentSearches({inputElement}: RecentSearchesProps) {
4651
*/
4752
const handleClearRecentSearchesClick = useCallback(() => {
4853
if (recentSearchesStore) {
49-
const updatedRecentSearches = recentSearchesStore.removeSearch()
50-
dispatch({recentSearches: updatedRecentSearches, type: 'RECENT_SEARCHES_SET'})
54+
recentSearchesStore.removeSearch()
5155
}
5256
commandListRef?.current?.focusInputElement()
53-
}, [dispatch, recentSearchesStore])
57+
}, [recentSearchesStore])
5458

5559
const mediaIndex = useMediaIndex()
5660

‎packages/sanity/src/core/studio/components/navbar/search/components/recentSearches/item/RecentSearchItem.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {type MouseEvent, useCallback} from 'react'
1414
import styled from 'styled-components'
1515

1616
import {useSearchState} from '../../../contexts/search/useSearchState'
17-
import {type RecentSearch} from '../../../datastores/recentSearches'
17+
import {type RecentSearch, useRecentSearchesStore} from '../../../datastores/recentSearches'
1818
import {DocumentTypesPill} from '../../common/DocumentTypesPill'
1919
import {FilterPill} from '../../common/FilterPill'
2020

@@ -60,7 +60,8 @@ export function RecentSearchItem({
6060
value,
6161
...rest
6262
}: RecentSearchesProps) {
63-
const {dispatch, recentSearchesStore} = useSearchState()
63+
const {dispatch} = useSearchState()
64+
const recentSearchesStore = useRecentSearchesStore()
6465

6566
// Determine how many characters are left to render type pills
6667
const availableCharacters = maxVisibleTypePillChars - value.query.length
@@ -70,8 +71,7 @@ export function RecentSearchItem({
7071

7172
// Add to Local Storage
7273
if (recentSearchesStore) {
73-
const updatedRecentSearches = recentSearchesStore?.addSearch(value, value?.filters)
74-
dispatch({recentSearches: updatedRecentSearches, type: 'RECENT_SEARCHES_SET'})
74+
recentSearchesStore?.addSearch(value, value?.filters)
7575
}
7676
}, [dispatch, recentSearchesStore, value])
7777

@@ -80,11 +80,10 @@ export function RecentSearchItem({
8080
event.stopPropagation()
8181
// Remove from Local Storage
8282
if (recentSearchesStore) {
83-
const updatedRecentSearches = recentSearchesStore?.removeSearchAtIndex(index)
84-
dispatch({recentSearches: updatedRecentSearches, type: 'RECENT_SEARCHES_SET'})
83+
recentSearchesStore?.removeSearchAtIndex(index)
8584
}
8685
},
87-
[dispatch, index, recentSearchesStore],
86+
[index, recentSearchesStore],
8887
)
8988

9089
return (

‎packages/sanity/src/core/studio/components/navbar/search/components/searchResults/SearchResults.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {useTranslation} from '../../../../../../i18n'
77
import {type WeightedHit} from '../../../../../../search'
88
import {getPublishedId} from '../../../../../../util/draftUtils'
99
import {useSearchState} from '../../contexts/search/useSearchState'
10+
import {useRecentSearchesStore} from '../../datastores/recentSearches'
1011
import {NoResults} from '../NoResults'
1112
import {SearchError} from '../SearchError'
1213
import {SortMenu} from '../SortMenu'
@@ -35,11 +36,11 @@ export function SearchResults({disableIntentLink, inputElement, onItemSelect}: S
3536
const {
3637
dispatch,
3738
onClose,
38-
recentSearchesStore,
3939
setSearchCommandList,
4040
state: {debug, filters, fullscreen, lastActiveIndex, result, terms},
4141
} = useSearchState()
4242
const {t} = useTranslation()
43+
const recentSearchesStore = useRecentSearchesStore()
4344

4445
const hasSearchResults = !!result.hits.length
4546
const hasNoSearchResults = !result.hits.length && result.loaded
@@ -50,11 +51,10 @@ export function SearchResults({disableIntentLink, inputElement, onItemSelect}: S
5051
*/
5152
const handleSearchResultClick = useCallback(() => {
5253
if (recentSearchesStore) {
53-
const updatedRecentSearches = recentSearchesStore.addSearch(terms, filters)
54-
dispatch({recentSearches: updatedRecentSearches, type: 'RECENT_SEARCHES_SET'})
54+
recentSearchesStore.addSearch(terms, filters)
5555
}
5656
onClose?.()
57-
}, [dispatch, filters, onClose, recentSearchesStore, terms])
57+
}, [filters, onClose, recentSearchesStore, terms])
5858

5959
const handleEndReached = useCallback(() => {
6060
dispatch({type: 'PAGE_INCREMENT'})

‎packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchContext.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {createContext, type Dispatch, type SetStateAction} from 'react'
22

33
import {type CommandListHandle} from '../../../../../../components/commandList/types'
4-
import {type RecentSearchesStore} from '../../datastores/recentSearches'
54
import {type SearchAction, type SearchReducerState} from './reducer'
65

76
/**
@@ -10,7 +9,6 @@ import {type SearchAction, type SearchReducerState} from './reducer'
109
export interface SearchContextValue {
1110
dispatch: Dispatch<SearchAction>
1211
onClose: (() => void) | null
13-
recentSearchesStore?: RecentSearchesStore
1412
searchCommandList: CommandListHandle | null
1513
setSearchCommandList: Dispatch<SetStateAction<CommandListHandle | null>>
1614
setOnClose: (onClose: () => void) => void

‎packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx

+3-49
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,12 @@ import isEqual from 'lodash/isEqual'
22
import {type ReactNode, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'
33

44
import {type CommandListHandle} from '../../../../../../components'
5-
import {useClient, useSchema} from '../../../../../../hooks'
5+
import {useSchema} from '../../../../../../hooks'
66
import {type SearchableType, type SearchTerms} from '../../../../../../search'
77
import {useCurrentUser} from '../../../../../../store'
8-
import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../../../../studioClient'
98
import {useSource} from '../../../../../source'
109
import {SEARCH_LIMIT} from '../../constants'
11-
import {
12-
createRecentSearchesStore,
13-
RECENT_SEARCH_VERSION,
14-
type RecentSearch,
15-
} from '../../datastores/recentSearches'
10+
import {type RecentSearch} from '../../datastores/recentSearches'
1611
import {createFieldDefinitionDictionary, createFieldDefinitions} from '../../definitions/fields'
1712
import {createFilterDefinitionDictionary} from '../../definitions/filters'
1813
import {createOperatorDefinitionDictionary} from '../../definitions/operators'
@@ -36,15 +31,12 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) {
3631
const onCloseRef = useRef<(() => void) | null>(null)
3732
const [searchCommandList, setSearchCommandList] = useState<CommandListHandle | null>(null)
3833

39-
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
4034
const schema = useSchema()
4135
const currentUser = useCurrentUser()
4236
const {
4337
search: {operators, filters},
4438
} = useSource()
4539

46-
const {dataset, projectId} = client.config()
47-
4840
// Create field, filter and operator dictionaries
4941
const {fieldDefinitions, filterDefinitions, operatorDefinitions} = useMemo(() => {
5042
return {
@@ -54,41 +46,11 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) {
5446
}
5547
}, [filters, operators, schema])
5648

57-
// Create local storage store
58-
const recentSearchesStore = useMemo(
59-
() =>
60-
createRecentSearchesStore({
61-
dataset,
62-
fieldDefinitions,
63-
filterDefinitions,
64-
operatorDefinitions,
65-
projectId,
66-
schema,
67-
user: currentUser,
68-
version: RECENT_SEARCH_VERSION,
69-
}),
70-
[
71-
currentUser,
72-
dataset,
73-
fieldDefinitions,
74-
filterDefinitions,
75-
operatorDefinitions,
76-
projectId,
77-
schema,
78-
],
79-
)
80-
81-
const recentSearches = useMemo(
82-
() => recentSearchesStore?.getRecentSearches(),
83-
[recentSearchesStore],
84-
)
85-
8649
const initialState = useMemo(
8750
() =>
8851
initialSearchState({
8952
currentUser,
9053
fullscreen,
91-
recentSearches,
9254
definitions: {
9355
fields: fieldDefinitions,
9456
operators: operatorDefinitions,
@@ -99,14 +61,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) {
9961
nextCursor: null,
10062
},
10163
}),
102-
[
103-
currentUser,
104-
fieldDefinitions,
105-
filterDefinitions,
106-
fullscreen,
107-
operatorDefinitions,
108-
recentSearches,
109-
],
64+
[currentUser, fieldDefinitions, filterDefinitions, fullscreen, operatorDefinitions],
11065
)
11166
const [state, dispatch] = useReducer(searchReducer, initialState)
11267

@@ -224,7 +179,6 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) {
224179
value={{
225180
dispatch,
226181
onClose: onCloseRef?.current,
227-
recentSearchesStore,
228182
searchCommandList,
229183
setSearchCommandList,
230184
setOnClose: handleSetOnClose,

‎packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.ts

-14
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export type SearchReducerState = PaginationState & {
3838
lastAddedFilter?: SearchFilter | null
3939
lastActiveIndex: number
4040
ordering: SearchOrdering
41-
recentSearches: RecentSearch[]
4241
result: SearchResult
4342
terms: RecentSearch | SearchTerms
4443
}
@@ -60,15 +59,13 @@ export interface SearchResult {
6059
export interface InitialSearchState {
6160
currentUser: CurrentUser | null
6261
fullscreen?: boolean
63-
recentSearches?: RecentSearch[]
6462
definitions: SearchDefinitions
6563
pagination: PaginationState
6664
}
6765

6866
export function initialSearchState({
6967
currentUser,
7068
fullscreen,
71-
recentSearches = [],
7269
definitions,
7370
pagination,
7471
}: InitialSearchState): SearchReducerState {
@@ -82,7 +79,6 @@ export function initialSearchState({
8279
lastActiveIndex: -1,
8380
ordering: ORDERINGS.relevance,
8481
...pagination,
85-
recentSearches,
8682
result: {
8783
error: null,
8884
hasLocal: false,
@@ -101,10 +97,6 @@ export function initialSearchState({
10197
export type FiltersVisibleSet = {type: 'FILTERS_VISIBLE_SET'; visible: boolean}
10298
export type LastActiveIndexSet = {type: 'LAST_ACTIVE_INDEX_SET'; index: number}
10399
export type PageIncrement = {type: 'PAGE_INCREMENT'}
104-
export type RecentSearchesSet = {
105-
recentSearches: RecentSearch[]
106-
type: 'RECENT_SEARCHES_SET'
107-
}
108100
export type OrderingReset = {type: 'ORDERING_RESET'}
109101
export type OrderingSet = {ordering: SearchOrdering; type: 'ORDERING_SET'}
110102
export type SearchClear = {type: 'SEARCH_CLEAR'}
@@ -140,7 +132,6 @@ export type SearchAction =
140132
| OrderingReset
141133
| OrderingSet
142134
| PageIncrement
143-
| RecentSearchesSet
144135
| SearchClear
145136
| SearchRequestComplete
146137
| SearchRequestError
@@ -198,11 +189,6 @@ export function searchReducer(state: SearchReducerState, action: SearchAction):
198189
nextCursor: null,
199190
terms: stripRecent(state.terms),
200191
}
201-
case 'RECENT_SEARCHES_SET':
202-
return {
203-
...state,
204-
recentSearches: action.recentSearches,
205-
}
206192
case 'SEARCH_CLEAR':
207193
return {
208194
...state,

‎packages/sanity/src/core/studio/components/navbar/search/datastores/recentSearches.test.ts

-358
This file was deleted.

‎packages/sanity/src/core/studio/components/navbar/search/datastores/recentSearches.test.tsx

+459
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1-
import {type CurrentUser, type ObjectSchemaType, type Schema} from '@sanity/types'
1+
import {type ObjectSchemaType, type Schema} from '@sanity/types'
22
import omit from 'lodash/omit'
3+
import {useMemo} from 'react'
34

5+
import {useSchema} from '../../../../../hooks'
46
import {type SearchTerms} from '../../../../../search'
5-
import {supportsLocalStorage} from '../../../../../util/supportsLocalStorage'
6-
import {type SearchFieldDefinitionDictionary} from '../definitions/fields'
7-
import {type SearchFilterDefinitionDictionary} from '../definitions/filters'
8-
import {type SearchOperatorDefinitionDictionary} from '../definitions/operators'
7+
import {useSource} from '../../../../source'
8+
import {
9+
createFieldDefinitionDictionary,
10+
createFieldDefinitions,
11+
type SearchFieldDefinitionDictionary,
12+
} from '../definitions/fields'
13+
import {
14+
createFilterDefinitionDictionary,
15+
type SearchFilterDefinitionDictionary,
16+
} from '../definitions/filters'
17+
import {
18+
createOperatorDefinitionDictionary,
19+
type SearchOperatorDefinitionDictionary,
20+
} from '../definitions/operators'
921
import {type SearchFilter} from '../types'
1022
import {validateFilter} from '../utils/filterUtils'
1123
import {getSearchableOmnisearchTypes} from '../utils/selectors'
24+
import {useStoredSearch} from './useStoredSearch'
1225

13-
const RECENT_SEARCHES_KEY = 'search::recent'
1426
export const MAX_RECENT_SEARCHES = 5
1527
/**
1628
* Current recent search version.
@@ -50,37 +62,28 @@ interface StoredSearchItem {
5062
terms: Omit<SearchTerms, 'types'> & {typeNames: string[]}
5163
}
5264

53-
export function createRecentSearchesStore({
54-
dataset,
55-
fieldDefinitions,
56-
filterDefinitions,
57-
operatorDefinitions,
58-
projectId,
59-
schema,
60-
user,
61-
version = RECENT_SEARCH_VERSION,
62-
}: {
63-
dataset?: string
64-
fieldDefinitions: SearchFieldDefinitionDictionary
65-
filterDefinitions: SearchFilterDefinitionDictionary
66-
operatorDefinitions: SearchOperatorDefinitionDictionary
67-
projectId?: string
68-
schema: Schema
69-
user: CurrentUser | null
70-
version: number
71-
}): RecentSearchesStore | undefined {
72-
if (!dataset || !projectId || !supportsLocalStorage || !user) {
73-
return undefined
74-
}
65+
export function useRecentSearchesStore(): RecentSearchesStore {
66+
const [storedSearch, setStoredSearch] = useStoredSearch()
67+
const schema = useSchema()
68+
const {
69+
search: {operators, filters},
70+
} = useSource()
7571

76-
const lsKey = `${RECENT_SEARCHES_KEY}__${projectId}:${dataset}:${user.id}`
72+
// Create field, filter and operator dictionaries
73+
const {fieldDefinitions, filterDefinitions, operatorDefinitions} = useMemo(() => {
74+
return {
75+
fieldDefinitions: createFieldDefinitionDictionary(createFieldDefinitions(schema, filters)),
76+
filterDefinitions: createFilterDefinitionDictionary(filters),
77+
operatorDefinitions: createOperatorDefinitionDictionary(operators),
78+
}
79+
}, [filters, operators, schema])
7780

7881
return {
7982
/**
8083
* Write a search term to Local Storage and return updated recent searches.
8184
*/
82-
addSearch: (searchTerm: SearchTerms, filters?: SearchFilter[]): RecentSearch[] => {
83-
const storedFilters = (filters || []).map(
85+
addSearch: (searchTerm: SearchTerms, searchFilters?: SearchFilter[]): RecentSearch[] => {
86+
const storedFilters = (searchFilters || []).map(
8487
(filter): SearchFilter => ({
8588
fieldId: filter.fieldId,
8689
filterName: filter.filterName,
@@ -111,23 +114,23 @@ export function createRecentSearchesStore({
111114
// When comparing search items, don't compare against the created date (which will always be different).
112115
const comparator = JSON.stringify(omit(newSearchItem, 'created'))
113116
const newRecent: StoredSearch = {
114-
version,
117+
version: RECENT_SEARCH_VERSION,
115118
recentSearches: [
116119
newSearchItem,
117-
...getRecentStoredSearch(lsKey, version).recentSearches.filter((r) => {
120+
...storedSearch.recentSearches.filter((r) => {
118121
return JSON.stringify(omit(r, 'created')) !== comparator
119122
}),
120123
].slice(0, MAX_RECENT_SEARCHES),
121124
}
122-
window.localStorage.setItem(lsKey, JSON.stringify(newRecent))
125+
setStoredSearch(newRecent)
123126

124127
return getRecentSearchTerms({
125128
fieldDefinitions,
126129
filterDefinitions,
127-
lsKey,
128130
operatorDefinitions,
129131
schema,
130-
version,
132+
storedSearch: newRecent,
133+
setStoredSearch,
131134
})
132135
},
133136
/**
@@ -138,114 +141,94 @@ export function createRecentSearchesStore({
138141
getRecentSearchTerms({
139142
fieldDefinitions,
140143
filterDefinitions,
141-
lsKey,
142144
operatorDefinitions,
143145
schema,
144-
version,
146+
storedSearch,
147+
setStoredSearch,
145148
}),
146149
/**
147150
* Remove all search terms from Local Storage and return updated recent searches.
148151
*/
149152
removeSearch: () => {
150-
const searchTerms = getRecentStoredSearch(lsKey, version)
151-
152153
const newRecent: StoredSearch = {
153-
...searchTerms,
154+
...storedSearch,
154155
recentSearches: [],
155156
}
156157

157-
window.localStorage.setItem(lsKey, JSON.stringify(newRecent))
158+
setStoredSearch(newRecent)
158159

159160
return getRecentSearchTerms({
160161
fieldDefinitions,
161162
filterDefinitions,
162-
lsKey,
163163
operatorDefinitions,
164164
schema,
165-
version,
165+
storedSearch: newRecent,
166+
setStoredSearch,
166167
})
167168
},
168169
/**
169170
* Remove a search term from Local Storage and return updated recent searches.
170171
*/
171172
removeSearchAtIndex: (index: number) => {
172-
const searchTerms = getRecentStoredSearch(lsKey, version)
173-
174-
if (index < 0 || index > searchTerms.recentSearches.length) {
173+
if (index < 0 || index > storedSearch.recentSearches.length) {
175174
return getRecentSearchTerms({
176175
fieldDefinitions,
177176
filterDefinitions,
178-
lsKey,
179177
operatorDefinitions,
180178
schema,
181-
version,
179+
storedSearch,
180+
setStoredSearch,
182181
})
183182
}
184183

185184
const newRecent: StoredSearch = {
186-
...searchTerms,
185+
...storedSearch,
187186
recentSearches: [
188-
...searchTerms.recentSearches.slice(0, index),
189-
...searchTerms.recentSearches.slice(index + 1),
187+
...storedSearch.recentSearches.slice(0, index),
188+
...storedSearch.recentSearches.slice(index + 1),
190189
],
191190
}
192191

193-
window.localStorage.setItem(lsKey, JSON.stringify(newRecent))
192+
setStoredSearch(newRecent)
194193

195194
return getRecentSearchTerms({
196195
fieldDefinitions,
197196
filterDefinitions,
198-
lsKey,
199197
operatorDefinitions,
200198
schema,
201-
version,
199+
storedSearch: newRecent,
200+
setStoredSearch,
202201
})
203202
},
204203
}
205204
}
206205

207-
/**
208-
* Get the 'raw' stored search terms from Local Storage.
209-
* Stored search terms are the minimal representation of saved terms and only include schema names.
210-
*/
211-
function getRecentStoredSearch(lsKey: string, version: number): StoredSearch {
212-
const recentString = supportsLocalStorage ? window.localStorage.getItem(lsKey) : undefined
213-
214-
return recentString ? (JSON.parse(recentString) as StoredSearch) : {version, recentSearches: []}
215-
}
216-
217206
/**
218207
* Get a list of recent searches from Local Storage.
219208
* Recent searches contain full document schema types.
220209
*/
221210
function getRecentSearchTerms({
222-
lsKey,
223211
schema,
224212
fieldDefinitions,
225213
filterDefinitions,
226214
operatorDefinitions,
227-
version,
215+
storedSearch,
216+
setStoredSearch,
228217
}: {
229-
lsKey: string
230218
schema: Schema
231219
fieldDefinitions: SearchFieldDefinitionDictionary
232220
filterDefinitions: SearchFilterDefinitionDictionary
233221
operatorDefinitions: SearchOperatorDefinitionDictionary
234-
version: number
222+
storedSearch: StoredSearch
223+
setStoredSearch: (_value: StoredSearch) => void
235224
}): RecentSearch[] {
236-
const storedSearchTerms = verifySearchVersionNumber({
237-
lsKey,
238-
storedSearch: getRecentStoredSearch(lsKey, version),
239-
})
240-
241225
return sanitizeStoredSearch({
242226
studioSchema: schema,
243-
storedSearch: storedSearchTerms,
244-
lsKey,
245227
filterDefinitions,
246228
fieldDefinitions,
247229
operatorDefinitions,
248-
version,
230+
storedSearch,
231+
setStoredSearch,
249232
})
250233
.recentSearches.filter((r) => !!r.terms)
251234
.map((r, index) => ({
@@ -261,34 +244,6 @@ function getRecentSearchTerms({
261244
}))
262245
}
263246

264-
/**
265-
* Check if there's a mismatch between the _search_ version in Local Storage and
266-
* the current studio.
267-
*
268-
* If there's a mismatch, clear all recent searches and update the stored search version.
269-
*
270-
* This mutates Local Storage if a mismatch is found.
271-
*/
272-
273-
function verifySearchVersionNumber({
274-
lsKey,
275-
storedSearch,
276-
}: {
277-
lsKey: string
278-
storedSearch: StoredSearch
279-
}): StoredSearch {
280-
if (storedSearch.version !== RECENT_SEARCH_VERSION) {
281-
const newStoredSearch: StoredSearch = {
282-
version: RECENT_SEARCH_VERSION,
283-
recentSearches: [],
284-
}
285-
window.localStorage.setItem(lsKey, JSON.stringify(newStoredSearch))
286-
return newStoredSearch
287-
}
288-
289-
return storedSearch
290-
}
291-
292247
/**
293248
* Sanitize stored search.
294249
*
@@ -302,19 +257,17 @@ function verifySearchVersionNumber({
302257
function sanitizeStoredSearch({
303258
fieldDefinitions,
304259
filterDefinitions,
305-
lsKey,
306260
operatorDefinitions,
307-
storedSearch,
308261
studioSchema,
309-
version,
262+
storedSearch,
263+
setStoredSearch,
310264
}: {
311265
fieldDefinitions: SearchFieldDefinitionDictionary
312266
filterDefinitions: SearchFilterDefinitionDictionary
313-
lsKey: string
314267
operatorDefinitions: SearchOperatorDefinitionDictionary
315-
storedSearch: StoredSearch
316268
studioSchema: Schema
317-
version: number
269+
storedSearch: StoredSearch
270+
setStoredSearch: (_value: StoredSearch) => void
318271
}): StoredSearch {
319272
// Obtain all 'searchable' type names – defined as a type that exists in
320273
// the current schema and also visible to omnisearch.
@@ -338,8 +291,8 @@ function sanitizeStoredSearch({
338291
}
339292

340293
if (newStoredSearch.recentSearches.length < storedSearch.recentSearches.length) {
341-
window.localStorage.setItem(lsKey, JSON.stringify(newStoredSearch))
294+
setStoredSearch(newStoredSearch)
342295
}
343296

344-
return getRecentStoredSearch(lsKey, version)
297+
return newStoredSearch
345298
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {useCallback, useEffect, useMemo, useState} from 'react'
2+
import {map, startWith} from 'rxjs/operators'
3+
4+
import {useClient} from '../../../../../hooks'
5+
import {useCurrentUser, useKeyValueStore} from '../../../../../store'
6+
import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../../../studioClient'
7+
8+
export const RECENT_SEARCH_VERSION = 2
9+
const STORED_SEARCHES_NAMESPACE = 'search::recent'
10+
11+
interface StoredSearch {
12+
version: number
13+
recentSearches: any[]
14+
}
15+
16+
const defaultValue: StoredSearch = {
17+
version: RECENT_SEARCH_VERSION,
18+
recentSearches: [],
19+
}
20+
21+
export function useStoredSearch(): [StoredSearch, (_value: StoredSearch) => void] {
22+
const keyValueStore = useKeyValueStore()
23+
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
24+
const currentUser = useCurrentUser()
25+
const {dataset, projectId} = client.config()
26+
27+
const keyValueStoreKey = useMemo(
28+
() => `${STORED_SEARCHES_NAMESPACE}__${projectId}:${dataset}:${currentUser?.id}`,
29+
[currentUser, dataset, projectId],
30+
)
31+
32+
const [value, setValue] = useState<StoredSearch>(defaultValue)
33+
34+
const settings = useMemo(() => {
35+
return keyValueStore.getKey(keyValueStoreKey)
36+
}, [keyValueStore, keyValueStoreKey])
37+
38+
useEffect(() => {
39+
const sub = settings
40+
.pipe(
41+
startWith(defaultValue as any),
42+
map((data: StoredSearch) => {
43+
// Check if the version matches RECENT_SEARCH_VERSION
44+
if (data?.version !== RECENT_SEARCH_VERSION) {
45+
// If not, return the default object and mutate the store (per original verifySearchVersionNumber logic)
46+
keyValueStore.setKey(keyValueStoreKey, defaultValue as any)
47+
return defaultValue
48+
}
49+
// Otherwise, return the data as is
50+
return data
51+
}),
52+
)
53+
.subscribe({
54+
next: setValue,
55+
})
56+
57+
return () => sub?.unsubscribe()
58+
}, [settings, keyValueStore, keyValueStoreKey])
59+
60+
const set = useCallback(
61+
(newValue: StoredSearch) => {
62+
setValue(newValue)
63+
keyValueStore.setKey(keyValueStoreKey, newValue as any)
64+
},
65+
[keyValueStore, keyValueStoreKey],
66+
)
67+
68+
return useMemo(() => [value, set], [set, value])
69+
}

‎test/e2e/tests/navbar/search.spec.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,42 @@ import {test} from '@sanity/test'
33

44
test('searching creates saved searches', async ({page, createDraftDocument, baseURL}) => {
55
await createDraftDocument('/test/content/book')
6+
67
await page.getByTestId('field-title').getByTestId('string-input').fill('A searchable title')
78
await page.getByTestId('studio-search').click()
89

9-
await page.getByPlaceholder('Search', {exact: true}).fill('A search')
10+
await page.getByPlaceholder('Search', {exact: true}).fill('A se')
1011
await page.getByTestId('search-results').isVisible()
1112
await page.getByTestId('search-results').click()
1213

14+
//search query should be saved
1315
const localStorage = await page.evaluate(() => window.localStorage)
1416
const keyMatch = Object.keys(localStorage).find((key) => key.startsWith('search::recent'))
1517
const savedSearches = JSON.parse(localStorage[keyMatch!]).recentSearches
16-
expect(savedSearches[0].terms.query).toBe('A search')
18+
expect(savedSearches[0].terms.query).toBe('A se')
1719

20+
//search query should be saved after browsing
1821
await page.goto('https://example.com')
1922
await page.goto(baseURL ?? '/test/content')
2023
const postNavigationLocalStorage = await page.evaluate(() => window.localStorage)
21-
22-
//also include going to other studio / project id?
2324
const postNavigationSearches = JSON.parse(postNavigationLocalStorage[keyMatch!]).recentSearches
24-
expect(postNavigationSearches[0].terms.query).toBe('A search')
25+
expect(postNavigationSearches[0].terms.query).toBe('A se')
2526

27+
//search should save multiple queries
2628
await page.getByTestId('studio-search').click()
29+
await page.getByPlaceholder('Search', {exact: true}).fill('A search')
30+
await page.getByTestId('search-results').isVisible()
31+
await page.getByTestId('search-results').click()
2732

33+
//search queries should stack, most recent first
34+
await page.getByTestId('studio-search').click()
2835
await page.getByPlaceholder('Search', {exact: true}).fill('A searchable')
2936
await page.getByTestId('search-results').isVisible()
3037
await page.getByTestId('search-results').click()
3138

3239
const secondSearchStorage = await page.evaluate(() => window.localStorage)
33-
3440
const secondSearches = JSON.parse(secondSearchStorage[keyMatch!]).recentSearches
3541
expect(secondSearches[0].terms.query).toBe('A searchable')
42+
expect(secondSearches[1].terms.query).toBe('A search')
43+
expect(secondSearches[2].terms.query).toBe('A se')
3644
})

0 commit comments

Comments
 (0)
Please sign in to comment.