feat: implement empty state for search dropdown with localized messages

This commit is contained in:
yessenia
2026-02-14 14:09:51 +08:00
parent dc4d3de533
commit ccb34e1020
5 changed files with 60 additions and 20 deletions

View File

@ -28,6 +28,8 @@ vi.mock('#i18n', () => ({
'plugin.marketplace.searchDropdown.showAllResults': 'Show all search results', 'plugin.marketplace.searchDropdown.showAllResults': 'Show all search results',
'plugin.marketplace.searchDropdown.enter': 'Enter', 'plugin.marketplace.searchDropdown.enter': 'Enter',
'plugin.marketplace.searchDropdown.byAuthor': `by ${options?.author || ''}`, 'plugin.marketplace.searchDropdown.byAuthor': `by ${options?.author || ''}`,
'plugin.marketplace.searchDropdown.noMatchesTitle': 'No matches',
'plugin.marketplace.searchDropdown.noMatchesDesc': 'Try different filter options.',
} }
return translations[fullKey] || key return translations[fullKey] || key
}, },
@ -582,6 +584,22 @@ describe('SearchDropdown', () => {
expect(screen.getByText('Tool')).toBeInTheDocument() expect(screen.getByText('Tool')).toBeInTheDocument()
expect(screen.getByText('206 installs')).toBeInTheDocument() expect(screen.getByText('206 installs')).toBeInTheDocument()
}) })
it('should render empty state when no results', () => {
render(
<SearchDropdown
query="non-existent"
plugins={[]}
templates={[]}
creators={[]}
onShowAll={vi.fn()}
/>,
)
expect(screen.getByText('No matches')).toBeInTheDocument()
expect(screen.getByText('Try different filter options.')).toBeInTheDocument()
expect(screen.queryByText('Show all search results')).not.toBeInTheDocument()
})
}) })
describe('Interactions', () => { describe('Interactions', () => {

View File

@ -70,8 +70,8 @@ const SearchBoxWrapper = ({
[dropdownQuery.data?.creators.items], [dropdownQuery.data?.creators.items],
) )
const handleSubmit = () => { const handleSubmit = (queryOverride?: string) => {
const trimmed = draftSearch.trim() const trimmed = (queryOverride ?? draftSearch).trim()
if (!trimmed) if (!trimmed)
return return
@ -141,7 +141,7 @@ const SearchBoxWrapper = ({
templates={dropdownTemplates} templates={dropdownTemplates}
creators={dropdownCreators} creators={dropdownCreators}
includeSource={includeSource} includeSource={includeSource}
onShowAll={handleSubmit} onShowAll={() => handleSubmit(debouncedDraft)}
isLoading={dropdownQuery.isLoading} isLoading={dropdownQuery.isLoading}
/> />
</PortalToFollowElemContent> </PortalToFollowElemContent>

View File

@ -1,7 +1,7 @@
import type { Creator, Template } from '../../types' import type { Creator, Template } from '../../types'
import type { Plugin } from '@/app/components/plugins/types' import type { Plugin } from '@/app/components/plugins/types'
import { useTranslation } from '#i18n' import { useTranslation } from '#i18n'
import { RiArrowRightLine } from '@remixicon/react' import { RiArrowRightLine, RiFilter3Line } from '@remixicon/react'
import { Fragment } from 'react' import { Fragment } from 'react'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
@ -27,6 +27,16 @@ const DropdownSection = ({ title, children }: { title: string, children: React.R
</div> </div>
) )
const EmptyState = ({ title, description }: { title: string, description: string }) => (
<div className="flex flex-col items-center gap-2 px-3 py-6">
<RiFilter3Line className="h-6 w-6 text-text-empty-state-icon" />
<div className="flex flex-col items-center gap-1 text-center">
<div className="system-md-medium text-text-secondary">{title}</div>
<div className="system-xs-regular text-text-tertiary">{description}</div>
</div>
</div>
)
const DropdownItem = ({ href, icon, children }: { const DropdownItem = ({ href, icon, children }: {
href: string href: string
icon: React.ReactNode icon: React.ReactNode
@ -162,6 +172,12 @@ const SearchDropdown = ({
<Loading /> <Loading />
</div> </div>
)} )}
{!isLoading && !hasResults && (
<EmptyState
title={t('marketplace.searchDropdown.noMatchesTitle', { ns: 'plugin' })}
description={t('marketplace.searchDropdown.noMatchesDesc', { ns: 'plugin' })}
/>
)}
{sections.map((section, i) => ( {sections.map((section, i) => (
<Fragment key={i}> <Fragment key={i}>
@ -170,23 +186,25 @@ const SearchDropdown = ({
</Fragment> </Fragment>
))} ))}
</div> </div>
<div className="border-t border-divider-subtle p-1"> {hasResults && (
<button <div className="border-t border-divider-subtle p-1">
className="group flex w-full items-center justify-between rounded-lg px-3 py-2 text-left hover:bg-state-base-hover" <button
onClick={onShowAll} className="group flex w-full items-center justify-between rounded-lg px-3 py-2 text-left hover:bg-state-base-hover"
type="button" onClick={onShowAll}
> type="button"
<span className="system-sm-medium text-text-accent"> >
{t('marketplace.searchDropdown.showAllResults', { ns: 'plugin', query })} <span className="system-sm-medium text-text-accent">
</span> {t('marketplace.searchDropdown.showAllResults', { ns: 'plugin', query })}
<span className="flex items-center">
<span className="system-2xs-medium-uppercase rounded-[5px] border border-divider-deep px-1.5 py-0.5 text-text-tertiary group-hover:hidden">
{t('marketplace.searchDropdown.enter', { ns: 'plugin' })}
</span> </span>
<RiArrowRightLine className="hidden h-[18px] w-[18px] text-text-accent group-hover:block" /> <span className="flex items-center">
</span> <span className="system-2xs-medium-uppercase rounded-[5px] border border-divider-deep px-1.5 py-0.5 text-text-tertiary group-hover:hidden">
</button> {t('marketplace.searchDropdown.enter', { ns: 'plugin' })}
</div> </span>
<RiArrowRightLine className="hidden h-[18px] w-[18px] text-text-accent group-hover:block" />
</span>
</button>
</div>
)}
</div> </div>
) )
} }

View File

@ -220,6 +220,8 @@
"marketplace.searchBreadcrumbSearch": "Search", "marketplace.searchBreadcrumbSearch": "Search",
"marketplace.searchDropdown.byAuthor": "by {{author}}", "marketplace.searchDropdown.byAuthor": "by {{author}}",
"marketplace.searchDropdown.enter": "Enter", "marketplace.searchDropdown.enter": "Enter",
"marketplace.searchDropdown.noMatchesDesc": "Try different filter options.",
"marketplace.searchDropdown.noMatchesTitle": "No matches",
"marketplace.searchDropdown.plugins": "Plugins", "marketplace.searchDropdown.plugins": "Plugins",
"marketplace.searchDropdown.showAllResults": "Show all search results", "marketplace.searchDropdown.showAllResults": "Show all search results",
"marketplace.searchFilterAll": "All", "marketplace.searchFilterAll": "All",

View File

@ -220,6 +220,8 @@
"marketplace.searchBreadcrumbSearch": "搜索", "marketplace.searchBreadcrumbSearch": "搜索",
"marketplace.searchDropdown.byAuthor": "由 {{author}} 提供", "marketplace.searchDropdown.byAuthor": "由 {{author}} 提供",
"marketplace.searchDropdown.enter": "输入", "marketplace.searchDropdown.enter": "输入",
"marketplace.searchDropdown.noMatchesDesc": "尝试其他筛选条件。",
"marketplace.searchDropdown.noMatchesTitle": "无匹配结果",
"marketplace.searchDropdown.plugins": "插件", "marketplace.searchDropdown.plugins": "插件",
"marketplace.searchDropdown.showAllResults": "显示所有搜索结果", "marketplace.searchDropdown.showAllResults": "显示所有搜索结果",
"marketplace.searchFilterAll": "全部", "marketplace.searchFilterAll": "全部",