mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix: restore marketplace search interactions
This commit is contained in:
parent
b5082d2796
commit
ab3bb9dd6c
@ -0,0 +1,62 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import MarketplaceLiveSearch from '../marketplace-live-search'
|
||||
|
||||
const { mockReplace } = vi.hoisted(() => ({
|
||||
mockReplace: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('ahooks')>()
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDebounce: <T,>(value: T) => value,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ replace: mockReplace }),
|
||||
}))
|
||||
|
||||
describe('MarketplaceLiveSearch', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('updates the active tab result route while the user types', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<MarketplaceLiveSearch
|
||||
action="/templates/knowledge"
|
||||
language="en-US"
|
||||
placeholder="Search templates"
|
||||
query=""
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.type(screen.getByRole('searchbox'), 'legal')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenLastCalledWith('/templates/knowledge?q=legal&language=en-US', {
|
||||
scroll: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('clears the query without leaving the active plugin tab', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<MarketplaceLiveSearch action="/plugins/tool" placeholder="Search plugins" query="maps" />,
|
||||
)
|
||||
|
||||
await user.clear(screen.getByRole('searchbox'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenLastCalledWith('/plugins/tool', { scroll: false })
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,171 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
MarketplaceSearchAutocomplete,
|
||||
MarketplaceSearchForm,
|
||||
} from '../marketplace-search-autocomplete'
|
||||
|
||||
const { mockPluginSearch, mockTemplateSearch } = vi.hoisted(() => ({
|
||||
mockPluginSearch: vi.fn(),
|
||||
mockTemplateSearch: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('ahooks')>()
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDebounce: <T,>(value: T) => value,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) =>
|
||||
({
|
||||
'gotoAnything.searching': 'Searching...',
|
||||
'marketplace.noPluginFound': 'No integration found',
|
||||
'newApp.noTemplateFound': 'No templates found',
|
||||
})[key] ?? key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceQuery: {
|
||||
searchAdvanced: {
|
||||
queryOptions: ({ input }: { input: unknown }) => ({
|
||||
queryKey: ['marketplace', 'plugins', input],
|
||||
queryFn: () => mockPluginSearch(input),
|
||||
}),
|
||||
},
|
||||
templateSearch: {
|
||||
queryOptions: ({ input }: { input: unknown }) => ({
|
||||
queryKey: ['marketplace', 'templates', input],
|
||||
queryFn: () => mockTemplateSearch(input),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
let queryClient: QueryClient
|
||||
|
||||
function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
}
|
||||
|
||||
describe('MarketplaceSearchAutocomplete', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: 0,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
mockPluginSearch.mockResolvedValue({ data: { plugins: [], total: 0 } })
|
||||
mockTemplateSearch.mockResolvedValue({ data: { templates: [], total: 0 } })
|
||||
})
|
||||
|
||||
it('shows template suggestions and keeps the route search form contract', async () => {
|
||||
let resolveTemplateSearch!: (value: unknown) => void
|
||||
const templateSearchPromise = new Promise((resolve) => {
|
||||
resolveTemplateSearch = resolve
|
||||
})
|
||||
mockTemplateSearch.mockReturnValue(templateSearchPromise)
|
||||
const templateSearchResponse = {
|
||||
data: {
|
||||
templates: [
|
||||
{
|
||||
id: 'template-1',
|
||||
template_name: 'Legal Research Agent',
|
||||
overview: 'Research legal questions with cited sources.',
|
||||
publisher_handle: 'dify',
|
||||
usage_count: 120,
|
||||
categories: ['knowledge'],
|
||||
icon: '📄',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_file_key: '',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
}
|
||||
const user = userEvent.setup()
|
||||
|
||||
const { container } = render(
|
||||
<MarketplaceSearchForm
|
||||
action="/templates/knowledge"
|
||||
category="knowledge"
|
||||
language="en-US"
|
||||
locale="en-US"
|
||||
placeholder="Search all templates..."
|
||||
query=""
|
||||
scope="templates"
|
||||
/>,
|
||||
{ wrapper: Wrapper },
|
||||
)
|
||||
|
||||
await user.type(screen.getByRole('combobox'), 'legal')
|
||||
expect(screen.queryByText('Legal Research Agent')).not.toBeInTheDocument()
|
||||
resolveTemplateSearch(templateSearchResponse)
|
||||
|
||||
expect(await screen.findByText('Legal Research Agent')).toBeInTheDocument()
|
||||
expect(screen.getByText('Research legal questions with cited sources.')).toBeInTheDocument()
|
||||
expect(container.querySelector('form')).toHaveAttribute('action', '/templates/knowledge')
|
||||
expect(container.querySelector('input[role="combobox"]')).toHaveAttribute('name', 'q')
|
||||
expect(container.querySelector('input[type="hidden"]')).toHaveValue('en-US')
|
||||
expect(mockPluginSearch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows plugin suggestions while preserving the controlled search owner', 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 onValueChange = vi.fn()
|
||||
|
||||
const ControlledSearch = () => {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<MarketplaceSearchAutocomplete
|
||||
locale="en-US"
|
||||
onValueChange={(nextValue) => {
|
||||
onValueChange(nextValue)
|
||||
setValue(nextValue)
|
||||
}}
|
||||
placeholder="Search plugins"
|
||||
scope="plugins"
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render(<ControlledSearch />, { wrapper: Wrapper })
|
||||
|
||||
await user.type(screen.getByRole('combobox'), 'google')
|
||||
|
||||
expect(await screen.findByText('Google Search')).toBeInTheDocument()
|
||||
expect(screen.getByText('Search the web from your workflow.')).toBeInTheDocument()
|
||||
expect(onValueChange).toHaveBeenLastCalledWith('google')
|
||||
expect(mockTemplateSearch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -4,8 +4,8 @@ import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
import SearchBoxWrapper from '@/app/components/plugins/marketplace/search-box/search-box-wrapper'
|
||||
import styles from './home-sticky.module.css'
|
||||
import MarketplacePluginSearch from './marketplace-plugin-search'
|
||||
|
||||
const HomeSearch = ({ children }: { children?: ReactNode }) => {
|
||||
const searchRef = useRef<HTMLDivElement>(null)
|
||||
@ -32,14 +32,8 @@ const HomeSearch = ({ children }: { children?: ReactNode }) => {
|
||||
>
|
||||
<div ref={searchRef} className="pointer-events-auto relative w-full max-w-[420px]">
|
||||
{children ?? (
|
||||
<SearchBoxWrapper
|
||||
wrapperClassName="w-full max-w-none"
|
||||
inputClassName="h-9 w-full rounded-[10px] bg-components-input-bg-normal [&>div]:px-2.5"
|
||||
inputElementClassName="text-[14px] leading-5"
|
||||
searchIconName="i-ri-search-line"
|
||||
<MarketplacePluginSearch
|
||||
placeholder={t(($) => $['marketplace.home.searchPlaceholder'])}
|
||||
showTags={false}
|
||||
usedInMarketplace={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
|
||||
type MarketplaceLiveSearchProps = {
|
||||
action: string
|
||||
className?: string
|
||||
language?: string
|
||||
placeholder: string
|
||||
query: string
|
||||
}
|
||||
|
||||
export default function MarketplaceLiveSearch({
|
||||
action,
|
||||
className,
|
||||
language,
|
||||
placeholder,
|
||||
query,
|
||||
}: MarketplaceLiveSearchProps) {
|
||||
const router = useRouter()
|
||||
const [value, setValue] = useState(query)
|
||||
const debouncedSearch = useDebounce(value.trim(), { wait: 300 })
|
||||
const routedSearchRef = useRef(query.trim())
|
||||
const navigate = useCallback(
|
||||
(nextQuery: string) => {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (nextQuery) searchParams.set('q', nextQuery)
|
||||
if (language) searchParams.set('language', language)
|
||||
const queryString = searchParams.toString()
|
||||
|
||||
router.replace(`${action}${queryString ? `?${queryString}` : ''}`, { scroll: false })
|
||||
},
|
||||
[action, language, router],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch === routedSearchRef.current) return
|
||||
|
||||
routedSearchRef.current = debouncedSearch
|
||||
navigate(debouncedSearch)
|
||||
}, [debouncedSearch, navigate])
|
||||
|
||||
return (
|
||||
<form
|
||||
action={action}
|
||||
className={cn('relative shrink-0', className)}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const nextQuery = value.trim()
|
||||
routedSearchRef.current = nextQuery
|
||||
navigate(nextQuery)
|
||||
}}
|
||||
>
|
||||
<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) => 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"
|
||||
/>
|
||||
{language && <input type="hidden" name="language" value={language} />}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useActivePluginType, useSearchPluginText } from '../atoms'
|
||||
import { MarketplaceSearchAutocomplete } from './marketplace-search-autocomplete'
|
||||
|
||||
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)
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
scope="plugins"
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,261 @@
|
||||
'use client'
|
||||
|
||||
import type { MarketplacePlugin, MarketplaceTemplate } from '@dify/contracts/marketplace'
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteClear,
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteInput,
|
||||
AutocompleteInputGroup,
|
||||
AutocompleteItem,
|
||||
AutocompleteItemIndicator,
|
||||
AutocompleteItemText,
|
||||
AutocompleteList,
|
||||
AutocompleteStatus,
|
||||
} from '@langgenius/dify-ui/autocomplete'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { renderI18nObject } from '@/i18n-config/index'
|
||||
import { marketplaceQuery } from '@/service/client'
|
||||
|
||||
export type MarketplaceSearchScope = 'all' | 'plugins' | 'templates'
|
||||
|
||||
type MarketplaceSuggestion = {
|
||||
description: string
|
||||
id: string
|
||||
kind: 'plugin' | 'template'
|
||||
label: string
|
||||
meta: string
|
||||
}
|
||||
|
||||
type MarketplaceSearchAutocompleteProps = {
|
||||
category?: string
|
||||
inputName?: string
|
||||
locale: string
|
||||
onValueChange: (value: string) => void
|
||||
placeholder: string
|
||||
scope: MarketplaceSearchScope
|
||||
value: string
|
||||
}
|
||||
|
||||
const getPluginText = (
|
||||
value: MarketplacePlugin['brief'] | MarketplacePlugin['label'],
|
||||
locale: string,
|
||||
) => {
|
||||
if (typeof value === 'string') return value
|
||||
return renderI18nObject((value ?? {}) as Record<string, string>, locale)
|
||||
}
|
||||
|
||||
const toTemplateSuggestion = (template: MarketplaceTemplate): MarketplaceSuggestion => ({
|
||||
description: template.overview,
|
||||
id: `template:${template.id}`,
|
||||
kind: 'template',
|
||||
label: template.template_name,
|
||||
meta: template.publisher_handle || template.publisher_unique_handle || '',
|
||||
})
|
||||
|
||||
const toPluginSuggestion = (plugin: MarketplacePlugin, locale: string): MarketplaceSuggestion => ({
|
||||
description: getPluginText(plugin.brief, locale),
|
||||
id: `plugin:${plugin.org}/${plugin.name}`,
|
||||
kind: 'plugin',
|
||||
label: getPluginText(plugin.label, locale) || plugin.name,
|
||||
meta: plugin.org,
|
||||
})
|
||||
|
||||
export function MarketplaceSearchAutocomplete({
|
||||
category = 'all',
|
||||
inputName,
|
||||
locale,
|
||||
onValueChange,
|
||||
placeholder,
|
||||
scope,
|
||||
value,
|
||||
}: MarketplaceSearchAutocompleteProps) {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const translate = t as (key: string, options?: Record<string, unknown>) => string
|
||||
const debouncedSearch = useDebounce(value.trim(), { wait: 300 })
|
||||
const hasQuery = Boolean(debouncedSearch)
|
||||
const showDropdown = isOpen && hasQuery
|
||||
const searchesPlugins = scope === 'all' || scope === 'plugins'
|
||||
const searchesTemplates = scope === 'all' || scope === 'templates'
|
||||
const isBundleSearch = category === 'bundle'
|
||||
const pluginQuery = useQuery({
|
||||
...marketplaceQuery.searchAdvanced.queryOptions({
|
||||
input: {
|
||||
params: { kind: isBundleSearch ? 'bundles' : 'plugins' },
|
||||
body: {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
query: debouncedSearch,
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
category: category !== 'all' && !isBundleSearch ? category : '',
|
||||
},
|
||||
},
|
||||
retry: false,
|
||||
}),
|
||||
enabled: hasQuery && searchesPlugins,
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const templateQuery = useQuery({
|
||||
...marketplaceQuery.templateSearch.queryOptions({
|
||||
input: {
|
||||
body: {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
query: debouncedSearch,
|
||||
sort_by: 'usage_count',
|
||||
sort_order: 'DESC',
|
||||
...(category !== 'all' ? { categories: [category] } : {}),
|
||||
},
|
||||
},
|
||||
retry: false,
|
||||
}),
|
||||
enabled: hasQuery && searchesTemplates,
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const pluginSuggestions = searchesPlugins
|
||||
? (pluginQuery.data?.data.bundles ?? pluginQuery.data?.data.plugins ?? []).map((plugin) =>
|
||||
toPluginSuggestion(plugin, locale),
|
||||
)
|
||||
: []
|
||||
const templateSuggestions = searchesTemplates
|
||||
? (templateQuery.data?.data?.templates ?? []).map(toTemplateSuggestion)
|
||||
: []
|
||||
const suggestions = [...templateSuggestions, ...pluginSuggestions]
|
||||
const isSearching = pluginQuery.isFetching || templateQuery.isFetching
|
||||
const emptyText =
|
||||
scope === 'templates'
|
||||
? translate('newApp.noTemplateFound', { ns: 'app' })
|
||||
: translate('marketplace.noPluginFound', { ns: 'plugin' })
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
filter={null}
|
||||
itemToStringValue={(item) => item.label}
|
||||
items={suggestions}
|
||||
mode="list"
|
||||
name={inputName}
|
||||
onOpenChange={setIsOpen}
|
||||
onValueChange={(nextValue) => {
|
||||
onValueChange(nextValue)
|
||||
setIsOpen(Boolean(nextValue.trim()))
|
||||
}}
|
||||
open={showDropdown}
|
||||
openOnInputClick
|
||||
submitOnItemClick={Boolean(inputName)}
|
||||
value={value}
|
||||
>
|
||||
<AutocompleteInputGroup
|
||||
size="large"
|
||||
className="border-[0.5px] border-components-input-border-active"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="ml-3 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder"
|
||||
/>
|
||||
<AutocompleteInput
|
||||
aria-label={placeholder}
|
||||
className="px-2 text-sm"
|
||||
placeholder={placeholder}
|
||||
size="large"
|
||||
type="search"
|
||||
/>
|
||||
{!!value && (
|
||||
<AutocompleteClear
|
||||
aria-label={translate('clearSearch', { ns: 'plugin', label: placeholder })}
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent
|
||||
sideOffset={8}
|
||||
portalProps={{ hidden: !showDropdown }}
|
||||
popupClassName="max-w-[420px]"
|
||||
popupProps={{ 'aria-busy': isSearching || undefined }}
|
||||
>
|
||||
{isSearching && suggestions.length === 0 && (
|
||||
<AutocompleteStatus>
|
||||
{translate('gotoAnything.searching', { ns: 'app' })}
|
||||
</AutocompleteStatus>
|
||||
)}
|
||||
<AutocompleteList<MarketplaceSuggestion>>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.id} value={item} className="items-start py-2">
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'mt-0.5 size-4 shrink-0 text-text-tertiary',
|
||||
item.kind === 'template' ? 'i-ri-layout-grid-line' : 'i-ri-puzzle-2-line',
|
||||
)}
|
||||
/>
|
||||
<span className="flex min-w-0 grow flex-col gap-0.5">
|
||||
<AutocompleteItemText className="px-0 text-text-primary">
|
||||
{item.label}
|
||||
</AutocompleteItemText>
|
||||
{!!item.description && (
|
||||
<span className="line-clamp-2 system-xs-regular text-text-tertiary">
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
{!!item.meta && (
|
||||
<span className="truncate system-xs-regular text-text-quaternary">
|
||||
{item.meta}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<AutocompleteItemIndicator />
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
{!isSearching && <AutocompleteEmpty>{emptyText}</AutocompleteEmpty>}
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
)
|
||||
}
|
||||
|
||||
type MarketplaceSearchFormProps = {
|
||||
action: string
|
||||
category?: string
|
||||
className?: string
|
||||
language?: string
|
||||
locale: string
|
||||
placeholder: string
|
||||
query: string
|
||||
scope: MarketplaceSearchScope
|
||||
}
|
||||
|
||||
export function MarketplaceSearchForm({
|
||||
action,
|
||||
category,
|
||||
className,
|
||||
language,
|
||||
locale,
|
||||
placeholder,
|
||||
query,
|
||||
scope,
|
||||
}: MarketplaceSearchFormProps) {
|
||||
const [value, setValue] = useState(query)
|
||||
|
||||
return (
|
||||
<form action={action} className={cn('relative shrink-0', className)}>
|
||||
<MarketplaceSearchAutocomplete
|
||||
category={category}
|
||||
inputName="q"
|
||||
locale={locale}
|
||||
onValueChange={setValue}
|
||||
placeholder={placeholder}
|
||||
scope={scope}
|
||||
value={value}
|
||||
/>
|
||||
{language && <input type="hidden" name="language" value={language} />}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@ -18,6 +18,7 @@ import HomeSearch from '../home/home-search'
|
||||
import { HomeStickyStateProvider } from '../home/home-sticky-state-provider'
|
||||
import styles from '../home/home-sticky.module.css'
|
||||
import HomeTrending from '../home/home-trending'
|
||||
import { MarketplaceSearchForm } from '../home/marketplace-search-autocomplete'
|
||||
import { GRID_CLASS } from '../list/collection-constants'
|
||||
import pluginTypeStyles from '../plugin-type-switch.module.css'
|
||||
import { TEMPLATE_CATEGORIES } from './categories'
|
||||
@ -36,32 +37,6 @@ type EmbeddedTemplatesMarketplaceProps = {
|
||||
|
||||
type TemplateCategoryLabels = Record<TemplateCategory, string>
|
||||
|
||||
function TemplateSearchForm({
|
||||
action,
|
||||
placeholder,
|
||||
query,
|
||||
}: {
|
||||
action: string
|
||||
placeholder: string
|
||||
query: string
|
||||
}) {
|
||||
return (
|
||||
<form action={action} className="relative w-full shrink-0">
|
||||
<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"
|
||||
defaultValue={query}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function TemplateCategoryNavigation({
|
||||
activeCategory,
|
||||
ariaLabel,
|
||||
@ -206,10 +181,14 @@ export async function EmbeddedTemplatesMarketplace({
|
||||
subtitle={tExplore('apps.description' as never)}
|
||||
/>
|
||||
<HomeSearch>
|
||||
<TemplateSearchForm
|
||||
<MarketplaceSearchForm
|
||||
action={category === 'all' ? '/templates' : `/templates/${category}`}
|
||||
category={category}
|
||||
className="w-full"
|
||||
locale={locale}
|
||||
placeholder={tApp('newAppFromTemplate.searchAllTemplate' as never)}
|
||||
query={query}
|
||||
scope="templates"
|
||||
/>
|
||||
</HomeSearch>
|
||||
{banners.length > 0 && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user