Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const useAddAssetScreen = (
} = useGlobalSearch({
debounceMs: SEARCH_DEBOUNCE_TIME,
scopes: ['assets'],
remoteAssets: { hasCollectible },
remoteAssets: { hasCollectible, showOnEmptyQuery: true },
})
const results = searchResults.remoteAssets
const isError = false
Expand Down
9 changes: 9 additions & 0 deletions packages/assets/src/api/assets/__tests__/endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,13 @@ describe('searchAssets', () => {
expect(call.params).not.toHaveProperty('cursor')
expect(call.params).not.toHaveProperty('has_collectible')
})

test('omits q when query is empty so the backend returns suggestions', async () => {
queryClientMock.mockResolvedValue({ data: validSearchResponse })

await searchAssets({ query: '', network: 'mainnet' })

const call = queryClientMock.mock.calls[0][0]
expect(call.params).not.toHaveProperty('q')
})
})
5 changes: 4 additions & 1 deletion packages/assets/src/api/assets/search-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ export const searchAssets = async ({
hasCollectible,
}: SearchAssetsParams) => {
const params: Record<string, string | number> = {
q: query,
limit,
}

if (query.length > 0) {
params.q = query
}

if (cursor) {
params.cursor = cursor
}
Expand Down
75 changes: 75 additions & 0 deletions packages/search/src/hooks/__tests__/useGlobalSearch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,81 @@ describe('useGlobalSearch', () => {
expect(lastCall?.[1]?.enabled).toBe(false)
})

test('showOnEmptyQuery runs the remote search with no query and surfaces suggestions', () => {
mockAllAccounts.mockReturnValue([makeAccount('ALICE', 'Alice')])
mockFindContacts.mockReturnValue([])
setOwnedAssets([])
const suggestions = [
{
assetId: '31566704',
name: 'USD Coin',
unitName: 'USDC',
logo: null,
verificationTier: 'trusted',
usdValue: null,
type: 'standard_asset',
collectibleTitle: null,
collectibleImage: null,
collectionName: null,
},
]
mockUseAssetSearchQuery.mockReturnValue({
results: suggestions,
isLoading: false,
isError: false,
isFetchingNextPage: false,
hasNextPage: false,
fetchNextPage: vi.fn(),
})

const { result } = renderHook(
() =>
useGlobalSearch({
debounceMs: 0,
scopes: ['assets'],
remoteAssets: { showOnEmptyQuery: true },
}),
{ wrapper: makeWrapper() },
)

const lastCall =
mockUseAssetSearchQuery.mock.calls[
mockUseAssetSearchQuery.mock.calls.length - 1
]
expect(lastCall?.[0]).toBe('')
expect(lastCall?.[1]?.enabled).toBe(true)
expect(result.current.value).toBe('')
expect(result.current.results.remoteAssets).toEqual(suggestions)
expect(result.current.results.accounts).toEqual([])
expect(result.current.hasResults).toBe(true)
})

test('showOnEmptyQuery surfaces the remote loading state for the suggestion fetch', () => {
mockAllAccounts.mockReturnValue([])
mockFindContacts.mockReturnValue([])
setOwnedAssets([])
mockUseAssetSearchQuery.mockReturnValue({
results: [],
isLoading: true,
isError: false,
isFetchingNextPage: false,
hasNextPage: false,
fetchNextPage: vi.fn(),
})

const { result } = renderHook(
() =>
useGlobalSearch({
debounceMs: 0,
scopes: ['assets'],
remoteAssets: { showOnEmptyQuery: true },
}),
{ wrapper: makeWrapper() },
)

expect(result.current.isLoading).toBe(true)
})

test('remoteAssets option surfaces backend search results', async () => {
mockAllAccounts.mockReturnValue([])
mockFindContacts.mockReturnValue([])
Expand Down
25 changes: 20 additions & 5 deletions packages/search/src/hooks/useGlobalSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const DEFAULT_DEBOUNCE_MS = 300
export type RemoteAssetsOptions = {
/** Restrict remote asset results to collectibles (NFTs). */
hasCollectible?: boolean
/** When true, run the remote asset search even with an empty query so
* the backend returns a default suggestion list. Local sections
* (accounts, contacts, owned assets) remain empty until the user types. */
showOnEmptyQuery?: boolean
}

export type UseGlobalSearchOptions = {
Expand Down Expand Up @@ -87,15 +91,22 @@ export const useGlobalSearch = (
return allOwnedAssets.filter(assetFilter)
}, [includesAssets, allOwnedAssets, assetFilter])

const showRemoteOnEmpty = remoteAssetsOptions?.showOnEmptyQuery ?? false
const shouldRunRemoteAssets =
remoteAssetsEnabled && includesAssets && hasQuery
remoteAssetsEnabled && includesAssets && (hasQuery || showRemoteOnEmpty)
const remoteAssetQuery = useAssetSearchQuery(debouncedValue, {
hasCollectible: remoteAssetsOptions?.hasCollectible,
enabled: shouldRunRemoteAssets,
})

const results = useMemo<GlobalSearchResults>(() => {
if (!hasQuery) return EMPTY_GLOBAL_SEARCH_RESULTS
if (!hasQuery) {
if (!shouldRunRemoteAssets) return EMPTY_GLOBAL_SEARCH_RESULTS
return {
...EMPTY_GLOBAL_SEARCH_RESULTS,
remoteAssets: remoteAssetQuery.results ?? [],
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
if (!hasQuery && !shouldRunRemoteAssets) return EMPTY_GLOBAL_SEARCH_RESULTS
return {
...EMPTY_GLOBAL_SEARCH_RESULTS,
remoteAssets: remoteAssetQuery.results ?? [],
}


const lowered = debouncedValue.toLowerCase()
const compare = (a: string, b: string) =>
Expand Down Expand Up @@ -167,15 +178,19 @@ export const useGlobalSearch = (
results.remoteAssets.length > 0

const isRemoteLoading = shouldRunRemoteAssets && remoteAssetQuery.isLoading
const hasUserQuery = value.length > 0
const isLoadingTypedQuery =
hasUserQuery &&
(isDebouncing || isOwnedAssetsLoading || isRemoteLoading)
const isLoadingEmptyQuerySuggestions =
!hasUserQuery && showRemoteOnEmpty && isRemoteLoading

return {
value,
setValue,
results,
hasResults,
isLoading:
value.length > 0 &&
(isDebouncing || isOwnedAssetsLoading || isRemoteLoading),
isLoading: isLoadingTypedQuery || isLoadingEmptyQuerySuggestions,
hasNextRemotePage: shouldRunRemoteAssets
? remoteAssetQuery.hasNextPage
: false,
Expand Down
Loading