mirror of https://github.com/langgenius/dify.git
feat: add unit tests for getValidatedPluginCategory and enhance search box functionality
This commit is contained in:
parent
b241122cf7
commit
56c5739e4e
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getValidatedPluginCategory } from './constants'
|
||||
|
||||
describe('getValidatedPluginCategory', () => {
|
||||
it('returns agent-strategy when query value is agent-strategy', () => {
|
||||
expect(getValidatedPluginCategory('agent-strategy')).toBe('agent-strategy')
|
||||
})
|
||||
|
||||
it('returns valid category values unchanged', () => {
|
||||
expect(getValidatedPluginCategory('model')).toBe('model')
|
||||
expect(getValidatedPluginCategory('tool')).toBe('tool')
|
||||
expect(getValidatedPluginCategory('bundle')).toBe('bundle')
|
||||
})
|
||||
|
||||
it('falls back to all for invalid category values', () => {
|
||||
expect(getValidatedPluginCategory('agent')).toBe('all')
|
||||
expect(getValidatedPluginCategory('invalid-category')).toBe('all')
|
||||
})
|
||||
})
|
||||
|
|
@ -23,6 +23,7 @@ export const PLUGIN_TYPE_SEARCH_MAP = {
|
|||
type ValueOf<T> = T[keyof T]
|
||||
|
||||
export type ActivePluginType = ValueOf<typeof PLUGIN_TYPE_SEARCH_MAP>
|
||||
const VALID_PLUGIN_CATEGORIES = new Set<ActivePluginType>(Object.values(PLUGIN_TYPE_SEARCH_MAP))
|
||||
|
||||
export const PLUGIN_CATEGORY_WITH_COLLECTIONS = new Set<ActivePluginType>(
|
||||
[
|
||||
|
|
@ -31,7 +32,6 @@ export const PLUGIN_CATEGORY_WITH_COLLECTIONS = new Set<ActivePluginType>(
|
|||
],
|
||||
)
|
||||
|
||||
|
||||
export const TEMPLATE_CATEGORY_MAP = {
|
||||
all: CATEGORY_ALL,
|
||||
marketing: 'marketing',
|
||||
|
|
@ -46,8 +46,10 @@ export const TEMPLATE_CATEGORY_MAP = {
|
|||
export type ActiveTemplateCategory = typeof TEMPLATE_CATEGORY_MAP[keyof typeof TEMPLATE_CATEGORY_MAP]
|
||||
|
||||
export function getValidatedPluginCategory(category: string): ActivePluginType {
|
||||
const key = (category in PLUGIN_TYPE_SEARCH_MAP ? category : CATEGORY_ALL) as keyof typeof PLUGIN_TYPE_SEARCH_MAP
|
||||
return PLUGIN_TYPE_SEARCH_MAP[key]
|
||||
if (VALID_PLUGIN_CATEGORIES.has(category as ActivePluginType))
|
||||
return category as ActivePluginType
|
||||
|
||||
return CATEGORY_ALL
|
||||
}
|
||||
|
||||
export function getValidatedTemplateCategory(category: string): ActiveTemplateCategory {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MarketplaceCollection } from './types'
|
||||
import type { PluginCollection } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { act, render, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
|
@ -12,9 +12,9 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
|||
import { DEFAULT_SORT, PLUGIN_TYPE_SEARCH_MAP, SCROLL_BOTTOM_THRESHOLD } from './constants'
|
||||
import {
|
||||
getFormattedPlugin,
|
||||
getMarketplaceListCondition,
|
||||
getMarketplaceListFilterType,
|
||||
getPluginCondition,
|
||||
getPluginDetailLinkInMarketplace,
|
||||
getPluginFilterType,
|
||||
getPluginIconInMarketplace,
|
||||
getPluginLinkInMarketplace,
|
||||
} from './utils'
|
||||
|
|
@ -386,7 +386,7 @@ const createMockPluginList = (count: number): Plugin[] =>
|
|||
install_count: 1000 - i * 10,
|
||||
}))
|
||||
|
||||
const createMockCollection = (overrides?: Partial<MarketplaceCollection>): MarketplaceCollection => ({
|
||||
const createMockCollection = (overrides?: Partial<PluginCollection>): PluginCollection => ({
|
||||
name: 'test-collection',
|
||||
label: { 'en-US': 'Test Collection' },
|
||||
description: { 'en-US': 'Test collection description' },
|
||||
|
|
@ -540,57 +540,57 @@ describe('utils', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceListCondition', () => {
|
||||
describe('getPluginCondition', () => {
|
||||
it('should return category condition for tool', () => {
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.tool)).toBe('category=tool')
|
||||
expect(getPluginCondition(PluginCategoryEnum.tool)).toBe('category=tool')
|
||||
})
|
||||
|
||||
it('should return category condition for model', () => {
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.model)).toBe('category=model')
|
||||
expect(getPluginCondition(PluginCategoryEnum.model)).toBe('category=model')
|
||||
})
|
||||
|
||||
it('should return category condition for agent', () => {
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.agent)).toBe('category=agent-strategy')
|
||||
expect(getPluginCondition(PluginCategoryEnum.agent)).toBe('category=agent-strategy')
|
||||
})
|
||||
|
||||
it('should return category condition for datasource', () => {
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.datasource)).toBe('category=datasource')
|
||||
expect(getPluginCondition(PluginCategoryEnum.datasource)).toBe('category=datasource')
|
||||
})
|
||||
|
||||
it('should return category condition for trigger', () => {
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.trigger)).toBe('category=trigger')
|
||||
expect(getPluginCondition(PluginCategoryEnum.trigger)).toBe('category=trigger')
|
||||
})
|
||||
|
||||
it('should return endpoint category for extension', () => {
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.extension)).toBe('category=endpoint')
|
||||
expect(getPluginCondition(PluginCategoryEnum.extension)).toBe('category=endpoint')
|
||||
})
|
||||
|
||||
it('should return type condition for bundle', () => {
|
||||
expect(getMarketplaceListCondition('bundle')).toBe('type=bundle')
|
||||
expect(getPluginCondition('bundle')).toBe('type=bundle')
|
||||
})
|
||||
|
||||
it('should return empty string for all', () => {
|
||||
expect(getMarketplaceListCondition('all')).toBe('')
|
||||
expect(getPluginCondition('all')).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for unknown type', () => {
|
||||
expect(getMarketplaceListCondition('unknown')).toBe('')
|
||||
expect(getPluginCondition('unknown')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceListFilterType', () => {
|
||||
describe('getPluginFilterType', () => {
|
||||
it('should return undefined for all', () => {
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.all)).toBeUndefined()
|
||||
expect(getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.all)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return bundle for bundle', () => {
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.bundle)).toBe('bundle')
|
||||
expect(getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.bundle)).toBe('bundle')
|
||||
})
|
||||
|
||||
it('should return plugin for other categories', () => {
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.tool)).toBe('plugin')
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.model)).toBe('plugin')
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.agent)).toBe('plugin')
|
||||
expect(getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.tool)).toBe('plugin')
|
||||
expect(getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.model)).toBe('plugin')
|
||||
expect(getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.agent)).toBe('plugin')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ vi.mock('#i18n', () => ({
|
|||
return translations[fullKey] || key
|
||||
},
|
||||
}),
|
||||
useLocale: () => 'en-US',
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
|
|
@ -648,6 +649,30 @@ describe('SearchBoxWrapper', () => {
|
|||
|
||||
expect(mockHandleSearchTextChange).toHaveBeenCalledWith('new search')
|
||||
})
|
||||
|
||||
it('should clear committed search when input is emptied and blurred', () => {
|
||||
render(<SearchBoxWrapper />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
// Focus, type something, then clear and blur
|
||||
fireEvent.focus(input)
|
||||
fireEvent.change(input, { target: { value: 'test query' } })
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(mockHandleSearchTextChange).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should not clear committed search when input has content and blurred', () => {
|
||||
render(<SearchBoxWrapper />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.focus(input)
|
||||
fireEvent.change(input, { target: { value: 'still has text' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(mockHandleSearchTextChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Translation', () => {
|
||||
|
|
|
|||
|
|
@ -97,8 +97,13 @@ const SearchBoxWrapper = ({
|
|||
setIsFocused(true)
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!isHoveringDropdown)
|
||||
if (!isHoveringDropdown) {
|
||||
if (!draftSearch.trim()) {
|
||||
handleSearchTextChange('')
|
||||
setSearchMode(null)
|
||||
}
|
||||
setIsFocused(false)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter')
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
import type { Creator, Template } from '../../types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { useTranslation } from '#i18n'
|
||||
import type { Locale } from '@/i18n-config/language'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { Fragment } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useCategories } from '@/app/components/plugins/hooks'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../../plugin-type-icons'
|
||||
import { getCreatorAvatarUrl, getPluginDetailLinkInMarketplace } from '../../utils'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
const DROPDOWN_PANEL = 'w-[472px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-sm'
|
||||
const ICON_BOX_BASE = 'flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
|
||||
const ICON_BOX_BASE = 'flex shrink-0 items-center justify-center overflow-hidden border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
|
||||
|
||||
const SectionDivider = () => (
|
||||
<div className="border-t border-divider-subtle" />
|
||||
)
|
||||
|
||||
const DropdownSection = ({ title, children }: { title: string, children: React.ReactNode }) => (
|
||||
<div className="p-1">
|
||||
|
|
@ -25,24 +32,34 @@ const DropdownItem = ({ href, icon, children }: {
|
|||
icon: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<a className="flex gap-2 rounded-lg px-3 py-2 hover:bg-state-base-hover" href={href}>
|
||||
<a className="flex gap-1 rounded-lg py-1 pl-3 pr-1 hover:bg-state-base-hover" href={href}>
|
||||
{icon}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">{children}</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 p-1">{children}</div>
|
||||
</a>
|
||||
)
|
||||
|
||||
const IconBox = ({ shape, className, children }: {
|
||||
const IconBox = ({ shape, size = 'sm', className, style, children }: {
|
||||
shape: 'rounded-lg' | 'rounded-full'
|
||||
size?: 'sm' | 'md'
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<div className={`${ICON_BOX_BASE} ${shape} ${className ?? ''}`}>
|
||||
<div
|
||||
className={cn(
|
||||
ICON_BOX_BASE,
|
||||
shape,
|
||||
size === 'sm' ? 'h-7 w-7' : 'h-8 w-8',
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const ItemMeta = ({ items }: { items: (React.ReactNode | string)[] }) => (
|
||||
<div className="flex items-center gap-1.5 pt-0.5 text-text-tertiary">
|
||||
<div className="flex items-center gap-1.5 pt-1 text-text-tertiary">
|
||||
{items.filter(Boolean).map((item, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <span className="system-xs-regular">·</span>}
|
||||
|
|
@ -70,11 +87,48 @@ const SearchDropdown = ({
|
|||
isLoading = false,
|
||||
}: SearchDropdownProps) => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { categoriesMap } = useCategories(true)
|
||||
|
||||
const hasResults = plugins.length > 0 || templates.length > 0 || creators.length > 0
|
||||
|
||||
// Collect rendered sections with dividers between them
|
||||
const sections: React.ReactNode[] = []
|
||||
|
||||
if (templates.length > 0) {
|
||||
sections.push(
|
||||
<TemplatesSection
|
||||
key="templates"
|
||||
templates={templates}
|
||||
locale={locale}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
if (plugins.length > 0) {
|
||||
sections.push(
|
||||
<PluginsSection
|
||||
key="plugins"
|
||||
plugins={plugins}
|
||||
getValueFromI18nObject={getValueFromI18nObject}
|
||||
categoriesMap={categoriesMap}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
if (creators.length > 0) {
|
||||
sections.push(
|
||||
<CreatorsSection
|
||||
key="creators"
|
||||
creators={creators}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={DROPDOWN_PANEL}>
|
||||
<div className="flex flex-col">
|
||||
|
|
@ -84,104 +138,12 @@ const SearchDropdown = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{plugins.length > 0 && (
|
||||
<DropdownSection title={t('marketplace.searchDropdown.plugins', { ns: 'plugin' })}>
|
||||
{plugins.map((plugin) => {
|
||||
const title = getValueFromI18nObject(plugin.label) || plugin.name
|
||||
const description = getValueFromI18nObject(plugin.brief) || ''
|
||||
const categoryLabel = categoriesMap[plugin.category]?.label || plugin.category
|
||||
const installLabel = t('install', { ns: 'plugin', num: plugin.install_count || 0 })
|
||||
const author = plugin.org || plugin.author || ''
|
||||
const TypeIcon = MARKETPLACE_TYPE_ICON_COMPONENTS[plugin.category]
|
||||
const categoryNode = (
|
||||
<div className="flex items-center gap-1">
|
||||
{TypeIcon && <TypeIcon className="h-4 w-4 text-text-tertiary" />}
|
||||
<span>{categoryLabel}</span>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<DropdownItem
|
||||
key={`${plugin.org}/${plugin.name}`}
|
||||
href={getPluginDetailLinkInMarketplace(plugin)}
|
||||
icon={(
|
||||
<IconBox shape="rounded-lg">
|
||||
<img className="h-full w-full object-cover" src={plugin.icon} alt={title} />
|
||||
</IconBox>
|
||||
)}
|
||||
>
|
||||
<div className="system-sm-medium truncate text-text-primary">{title}</div>
|
||||
{!!description && (
|
||||
<div className="system-xs-regular truncate text-text-tertiary">{description}</div>
|
||||
)}
|
||||
<ItemMeta
|
||||
items={[
|
||||
categoryNode,
|
||||
t('marketplace.searchDropdown.byAuthor', { ns: 'plugin', author }),
|
||||
installLabel,
|
||||
]}
|
||||
/>
|
||||
</DropdownItem>
|
||||
)
|
||||
})}
|
||||
</DropdownSection>
|
||||
)}
|
||||
|
||||
{templates.length > 0 && (
|
||||
<DropdownSection title={t('templates', { ns: 'plugin' })}>
|
||||
{templates.map(template => (
|
||||
<DropdownItem
|
||||
key={template.template_id}
|
||||
href={getMarketplaceUrl(`/templates/${template.template_id}`)}
|
||||
icon={(
|
||||
<IconBox shape="rounded-lg" className="text-base">
|
||||
{template.icon || '📄'}
|
||||
</IconBox>
|
||||
)}
|
||||
>
|
||||
<div className="system-sm-medium truncate text-text-primary">{template.name}</div>
|
||||
<ItemMeta
|
||||
items={[
|
||||
t('marketplace.searchDropdown.byAuthor', { ns: 'plugin', author: template.author }),
|
||||
...(template.tags.length > 0
|
||||
? [<span className="system-xs-regular truncate">{template.tags.join(', ')}</span>]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownSection>
|
||||
)}
|
||||
|
||||
{creators.length > 0 && (
|
||||
<DropdownSection title={t('marketplace.searchFilterCreators', { ns: 'plugin' })}>
|
||||
{creators.map(creator => (
|
||||
<DropdownItem
|
||||
key={creator.unique_handle}
|
||||
href={getMarketplaceUrl(`/creators/${creator.unique_handle}`)}
|
||||
icon={(
|
||||
<IconBox shape="rounded-full">
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
src={getCreatorAvatarUrl(creator.unique_handle)}
|
||||
alt={creator.display_name}
|
||||
/>
|
||||
</IconBox>
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="system-sm-medium truncate text-text-primary">{creator.display_name}</span>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
@
|
||||
{creator.unique_handle}
|
||||
</span>
|
||||
</div>
|
||||
{!!creator.description && (
|
||||
<div className="system-xs-regular truncate text-text-tertiary">{creator.description}</div>
|
||||
)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownSection>
|
||||
)}
|
||||
{sections.map((section, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <SectionDivider />}
|
||||
{section}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-divider-subtle p-1">
|
||||
<button
|
||||
|
|
@ -204,4 +166,136 @@ const SearchDropdown = ({
|
|||
)
|
||||
}
|
||||
|
||||
/* ---------- Templates Section ---------- */
|
||||
|
||||
function TemplatesSection({ templates, locale, t }: {
|
||||
templates: Template[]
|
||||
locale: Locale
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
return (
|
||||
<DropdownSection title={t('templates', { ns: 'plugin' })}>
|
||||
{templates.map((template) => {
|
||||
const descriptionText = template.description[getLanguage(locale)] || template.description.en_US || ''
|
||||
const iconBgStyle = template.icon_background
|
||||
? { backgroundColor: template.icon_background }
|
||||
: undefined
|
||||
return (
|
||||
<DropdownItem
|
||||
key={template.template_id}
|
||||
href={getMarketplaceUrl(`/templates/${template.template_id}`)}
|
||||
icon={(
|
||||
<div className="flex shrink-0 items-start py-1">
|
||||
<IconBox shape="rounded-lg" style={iconBgStyle}>
|
||||
<span className="text-xl leading-[1.2]">{template.icon || '📄'}</span>
|
||||
</IconBox>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="system-md-medium truncate text-text-primary">{template.name}</div>
|
||||
{!!descriptionText && (
|
||||
<div className="system-xs-regular line-clamp-2 text-text-tertiary">{descriptionText}</div>
|
||||
)}
|
||||
<ItemMeta
|
||||
items={[
|
||||
t('marketplace.searchDropdown.byAuthor', { ns: 'plugin', author: template.author }),
|
||||
...(template.tags.length > 0
|
||||
? [<span key="tags" className="system-xs-regular truncate">{template.tags.join(', ')}</span>]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</DropdownItem>
|
||||
)
|
||||
})}
|
||||
</DropdownSection>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- Plugins Section ---------- */
|
||||
|
||||
function PluginsSection({ plugins, getValueFromI18nObject, categoriesMap, t }: {
|
||||
plugins: Plugin[]
|
||||
getValueFromI18nObject: ReturnType<typeof useRenderI18nObject>
|
||||
categoriesMap: Record<string, { label: string }>
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
return (
|
||||
<DropdownSection title={t('marketplace.searchDropdown.plugins', { ns: 'plugin' })}>
|
||||
{plugins.map((plugin) => {
|
||||
const title = getValueFromI18nObject(plugin.label) || plugin.name
|
||||
const description = getValueFromI18nObject(plugin.brief) || ''
|
||||
const categoryLabel = categoriesMap[plugin.category]?.label || plugin.category
|
||||
const installLabel = t('install', { ns: 'plugin', num: plugin.install_count || 0 })
|
||||
const author = plugin.org || plugin.author || ''
|
||||
const TypeIcon = MARKETPLACE_TYPE_ICON_COMPONENTS[plugin.category]
|
||||
const categoryNode = (
|
||||
<div className="flex items-center gap-1">
|
||||
{TypeIcon && <TypeIcon className="h-[14px] w-[14px] text-text-tertiary" />}
|
||||
<span className="system-xs-regular">{categoryLabel}</span>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<DropdownItem
|
||||
key={`${plugin.org}/${plugin.name}`}
|
||||
href={getPluginDetailLinkInMarketplace(plugin)}
|
||||
icon={(
|
||||
<div className="flex shrink-0 items-start py-1">
|
||||
<IconBox shape="rounded-lg">
|
||||
<img className="h-full w-full object-cover" src={plugin.icon} alt={title} />
|
||||
</IconBox>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="system-md-medium truncate text-text-primary">{title}</div>
|
||||
{!!description && (
|
||||
<div className="system-xs-regular line-clamp-2 text-text-tertiary">{description}</div>
|
||||
)}
|
||||
<ItemMeta
|
||||
items={[
|
||||
categoryNode,
|
||||
t('marketplace.searchDropdown.byAuthor', { ns: 'plugin', author }),
|
||||
installLabel,
|
||||
]}
|
||||
/>
|
||||
</DropdownItem>
|
||||
)
|
||||
})}
|
||||
</DropdownSection>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- Creators Section ---------- */
|
||||
|
||||
function CreatorsSection({ creators, t }: {
|
||||
creators: Creator[]
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
return (
|
||||
<DropdownSection title={t('marketplace.searchFilterCreators', { ns: 'plugin' })}>
|
||||
{creators.map(creator => (
|
||||
<a
|
||||
key={creator.unique_handle}
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2 hover:bg-state-base-hover"
|
||||
href={getMarketplaceUrl(`/creators/${creator.unique_handle}`)}
|
||||
>
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full border-[0.5px] border-divider-regular">
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
src={getCreatorAvatarUrl(creator.unique_handle)}
|
||||
alt={creator.display_name}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-px">
|
||||
<div className="system-md-medium truncate text-text-primary">{creator.display_name}</div>
|
||||
<div className="system-xs-regular truncate text-text-tertiary">
|
||||
@
|
||||
{creator.unique_handle}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</DropdownSection>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchDropdown
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export type Template = {
|
|||
name: string
|
||||
description: Record<string, string>
|
||||
icon: string
|
||||
icon_background?: string
|
||||
tags: string[]
|
||||
author: string
|
||||
created_at: string
|
||||
|
|
@ -207,6 +208,7 @@ export type UnifiedTemplateItem = {
|
|||
index_id: string
|
||||
template_name: string
|
||||
icon: string
|
||||
icon_background?: string
|
||||
icon_file_key: string
|
||||
categories: string[]
|
||||
overview: string
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export function mapTemplateDetailToTemplate(template: TemplateDetail): Template
|
|||
zh_Hans: descriptionText,
|
||||
},
|
||||
icon: template.icon || '',
|
||||
icon_background: template.icon_background || undefined,
|
||||
tags: template.categories || [],
|
||||
author: template.publisher_unique_handle || template.creator_email || '',
|
||||
created_at: template.created_at,
|
||||
|
|
@ -448,6 +449,7 @@ export function mapUnifiedTemplateToTemplate(item: UnifiedTemplateItem): Templat
|
|||
zh_Hans: descriptionText,
|
||||
},
|
||||
icon: item.icon || '',
|
||||
icon_background: item.icon_background || undefined,
|
||||
tags: item.categories || [],
|
||||
author: item.publisher_handle || '',
|
||||
created_at: item.created_at,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { act, render, renderHook, screen, waitFor } from '@testing-library/react
|
|||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { SCROLL_BOTTOM_THRESHOLD } from '@/app/components/plugins/marketplace/constants'
|
||||
import { getMarketplaceListCondition } from '@/app/components/plugins/marketplace/utils'
|
||||
import { getPluginCondition } from '@/app/components/plugins/marketplace/utils'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
|
@ -289,7 +289,7 @@ describe('useMarketplace', () => {
|
|||
await waitFor(() => {
|
||||
expect(mockQueryMarketplaceCollectionsAndPlugins).toHaveBeenCalledWith({
|
||||
category: PluginCategoryEnum.tool,
|
||||
condition: getMarketplaceListCondition(PluginCategoryEnum.tool),
|
||||
condition: getPluginCondition(PluginCategoryEnum.tool),
|
||||
exclude: ['plugin-c'],
|
||||
type: 'plugin',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
CreatorSearchParams,
|
||||
CreatorSearchResponse,
|
||||
GetCollectionTemplatesRequest,
|
||||
MarketplaceCollection,
|
||||
PluginCollection,
|
||||
PluginsSearchParams,
|
||||
SyncCreatorProfileRequest,
|
||||
TemplateCollection,
|
||||
|
|
@ -21,7 +21,7 @@ import type { Plugin, PluginsFromMarketplaceResponse } from '@/app/components/pl
|
|||
import { type } from '@orpc/contract'
|
||||
import { base } from './base'
|
||||
|
||||
export const collectionsContract = base
|
||||
export const pluginCollectionsContract = base
|
||||
.route({
|
||||
path: '/collections',
|
||||
method: 'GET',
|
||||
|
|
@ -34,7 +34,7 @@ export const collectionsContract = base
|
|||
.output(
|
||||
type<{
|
||||
data?: {
|
||||
collections?: MarketplaceCollection[]
|
||||
collections?: PluginCollection[]
|
||||
}
|
||||
}>(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {
|
|||
batchAddTemplatesToCollectionContract,
|
||||
clearCollectionTemplatesContract,
|
||||
collectionPluginsContract,
|
||||
collectionsContract,
|
||||
createTemplateCollectionContract,
|
||||
deleteTemplateCollectionContract,
|
||||
getCollectionTemplatesContract,
|
||||
|
|
@ -36,6 +35,7 @@ import {
|
|||
getTemplateCollectionContract,
|
||||
getTemplateDslFileContract,
|
||||
getTemplatesListContract,
|
||||
pluginCollectionsContract,
|
||||
searchAdvancedContract,
|
||||
searchCreatorsAdvancedContract,
|
||||
searchTemplatesAdvancedContract,
|
||||
|
|
@ -48,7 +48,7 @@ import {
|
|||
|
||||
export const marketplaceRouterContract = {
|
||||
plugins: {
|
||||
collections: collectionsContract,
|
||||
collections: pluginCollectionsContract,
|
||||
collectionPlugins: collectionPluginsContract,
|
||||
searchAdvanced: searchAdvancedContract,
|
||||
getPublisherPlugins: getPublisherPluginsContract,
|
||||
|
|
|
|||
Loading…
Reference in New Issue