mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
feat: implement empty state for search dropdown with localized messages
This commit is contained in:
@ -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', () => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": "全部",
|
||||||
|
|||||||
Reference in New Issue
Block a user