fix(web): keep embedded marketplace search live without suggestions

The public marketplace owns the suggestion dropdown. Console plugin and
template search keep updating results as the query changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
CodingOnStar 2026-08-16 16:28:01 +08:00
parent cc2aa8a5d9
commit ea6935060b
4 changed files with 87 additions and 19 deletions

View File

@ -0,0 +1,22 @@
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vite-plus/test'
import { renderWithNuqs } from '@/test/nuqs-testing'
import MarketplacePluginSearch from '../marketplace-plugin-search'
describe('MarketplacePluginSearch', () => {
it('updates the catalog query as the user types without opening suggestions', async () => {
const user = userEvent.setup()
const { onUrlUpdate } = renderWithNuqs(<MarketplacePluginSearch placeholder="Search plugins" />)
const input = screen.getByRole('searchbox', { name: 'Search plugins' })
await user.type(input, 'google')
expect(input).toHaveValue('google')
await waitFor(() => {
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('q')).toBe('google')
})
expect(screen.queryByRole('combobox')).not.toBeInTheDocument()
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
})
})

View File

@ -176,6 +176,46 @@ describe('MarketplaceSearchAutocomplete', () => {
expect(mockTemplateSearch).not.toHaveBeenCalled()
})
it('submits the route search form when a suggestion is chosen', async () => {
mockPluginSearch.mockResolvedValue({
data: {
plugins: [
{
type: 'plugin',
org: 'langgenius',
name: 'google-search',
label: { en_US: 'Google Search' },
brief: { en_US: 'Search the web from your workflow.' },
category: 'tool',
},
],
total: 1,
},
})
const user = userEvent.setup()
const handleSubmit = vi.fn((event: Event) => {
event.preventDefault()
})
const { container } = render(
<MarketplaceSearchForm
action="/plugins"
locale="en-US"
placeholder="Search plugins"
query=""
scope="plugins"
/>,
{ wrapper: Wrapper },
)
container.querySelector('form')?.addEventListener('submit', handleSubmit)
await user.type(screen.getByRole('combobox'), 'google')
await user.click(await screen.findByText('Google Search'))
expect(handleSubmit).toHaveBeenCalledOnce()
})
it('does not offer the previous term suggestions while a new search is pending', async () => {
const googleResponse = {
data: {

View File

@ -1,28 +1,37 @@
'use client'
import { useLocale } from '@/context/i18n'
import { useActivePluginType, useSearchPluginText } from '../atoms'
import { MarketplaceSearchAutocomplete } from './marketplace-search-autocomplete'
import { useSearchPluginText } from '../atoms'
type MarketplacePluginSearchProps = {
placeholder: string
}
export default function MarketplacePluginSearch({ placeholder }: MarketplacePluginSearchProps) {
const locale = useLocale()
const [category] = useActivePluginType()
const [value, setValue] = useSearchPluginText()
return (
<MarketplaceSearchAutocomplete
category={category}
locale={locale}
onValueChange={(nextValue) => {
void setValue(nextValue)
<form
className="relative w-full shrink-0"
onSubmit={(event) => {
event.preventDefault()
}}
placeholder={placeholder}
scope="plugins"
value={value}
/>
>
<span
aria-hidden
className="pointer-events-none absolute top-1/2 left-3 i-ri-search-line size-4 -translate-y-1/2 text-text-tertiary"
/>
<input
type="search"
name="q"
autoComplete="off"
aria-label={placeholder}
value={value}
onChange={(event) => {
void setValue(event.target.value)
}}
placeholder={placeholder}
className="h-9 w-full rounded-[10px] border-[0.5px] border-components-input-border-active bg-components-input-bg-normal py-2 pr-3 pl-9 text-sm text-text-primary outline-none placeholder:text-text-quaternary focus:border-state-accent-solid"
/>
</form>
)
}

View File

@ -18,7 +18,7 @@ import HomeHero from '../home/home-hero'
import HomeSearch from '../home/home-search'
import { HomeShell } from '../home/home-shell'
import styles from '../home/home-sticky.module.css'
import { MarketplaceSearchForm } from '../home/marketplace-search-autocomplete'
import MarketplaceLiveSearch from '../home/marketplace-live-search'
import { GRID_CLASS } from '../list/collection-constants'
import TemplateCard from './template-card'
import TemplateCategoryNavigation from './template-category-navigation'
@ -211,14 +211,11 @@ export async function EmbeddedTemplatesMarketplace({
}
search={
<HomeSearch enableSearchShortcut={false}>
<MarketplaceSearchForm
<MarketplaceLiveSearch
action={category === 'all' ? '/templates' : `/templates/${category}`}
category={category}
className="w-full"
locale={locale}
placeholder={tApp(($) => $['newAppFromTemplate.searchAllTemplate'], { ns: 'app' })}
query={query}
scope="templates"
/>
</HomeSearch>
}