feat: add component filter to marketplace page

This commit is contained in:
Junyan Qin
2025-11-20 19:46:33 +08:00
parent da323817f7
commit 20614b20b7
8 changed files with 111 additions and 29 deletions

View File

@@ -10,7 +10,8 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Search, Loader2 } from 'lucide-react';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { Search, Loader2, Wrench, AudioWaveform, Hash } from 'lucide-react';
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog';
@@ -38,6 +39,7 @@ function MarketPageContent({
const searchParams = useSearchParams();
const [searchQuery, setSearchQuery] = useState('');
const [componentFilter, setComponentFilter] = useState<string>('all');
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
@@ -127,23 +129,18 @@ function MarketPageContent({
try {
let response;
const { sortBy, sortOrder } = getCurrentSort();
const filterValue =
componentFilter === 'all' ? undefined : componentFilter;
if (isSearch && searchQuery.trim()) {
response = await getCloudServiceClientSync().searchMarketplacePlugins(
searchQuery.trim(),
page,
pageSize,
sortBy,
sortOrder,
);
} else {
response = await getCloudServiceClientSync().getMarketplacePlugins(
page,
pageSize,
sortBy,
sortOrder,
);
}
// Always use searchMarketplacePlugins to support component filtering
response = await getCloudServiceClientSync().searchMarketplacePlugins(
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
page,
pageSize,
sortBy,
sortOrder,
filterValue,
);
const data: ApiRespMarketplacePlugins = response;
const newPlugins = data.plugins.map(transformToVO);
@@ -168,7 +165,14 @@ function MarketPageContent({
setIsLoadingMore(false);
}
},
[searchQuery, pageSize, transformToVO, plugins.length, getCurrentSort],
[
searchQuery,
componentFilter,
pageSize,
transformToVO,
plugins.length,
getCurrentSort,
],
);
// 初始加载
@@ -213,10 +217,18 @@ function MarketPageContent({
// fetchPlugins will be called by useEffect when sortOption changes
}, []);
// 当排序选项变化时重新加载数据
// 组件筛选变化处理
const handleComponentFilterChange = useCallback((value: string) => {
setComponentFilter(value);
setCurrentPage(1);
setPlugins([]);
// fetchPlugins will be called by useEffect when componentFilter changes
}, []);
// 当排序选项或组件筛选变化时重新加载数据
useEffect(() => {
fetchPlugins(1, !!searchQuery.trim(), true);
}, [sortOption]);
}, [sortOption, componentFilter]);
// 处理URL参数检查是否需要打开插件详情对话框
useEffect(() => {
@@ -343,9 +355,59 @@ function MarketPageContent({
</div>
</div>
{/* Sort dropdown */}
<div className="flex items-center justify-center">
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
{/* Component filter and sort */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-3 sm:px-4">
{/* Component filter */}
<div className="flex flex-col sm:flex-row items-center gap-2">
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
{t('market.filterByComponent')}:
</span>
<ToggleGroup
type="single"
spacing={2}
size="sm"
value={componentFilter}
onValueChange={(value) => {
if (value) handleComponentFilterChange(value);
}}
className="justify-start"
>
<ToggleGroupItem
value="all"
aria-label="All components"
className="text-xs sm:text-sm"
>
{t('market.allComponents')}
</ToggleGroupItem>
<ToggleGroupItem
value="Tool"
aria-label="Tool"
className="text-xs sm:text-sm"
>
<Wrench className="h-4 w-4 mr-1" />
{t('plugins.componentName.Tool')}
</ToggleGroupItem>
<ToggleGroupItem
value="Command"
aria-label="Command"
className="text-xs sm:text-sm"
>
<Hash className="h-4 w-4 mr-1" />
{t('plugins.componentName.Command')}
</ToggleGroupItem>
<ToggleGroupItem
value="EventListener"
aria-label="EventListener"
className="text-xs sm:text-sm"
>
<AudioWaveform className="h-4 w-4 mr-1" />
{t('plugins.componentName.EventListener')}
</ToggleGroupItem>
</ToggleGroup>
</div>
{/* Sort dropdown */}
<div className="flex items-center gap-2 sm:gap-3">
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
{t('market.sortBy')}:
</span>

View File

@@ -34,6 +34,7 @@ export class CloudServiceClient extends BaseHttpClient {
page_size: number,
sort_by?: string,
sort_order?: string,
component_filter?: string,
): Promise<ApiRespMarketplacePlugins> {
return this.post<ApiRespMarketplacePlugins>(
'/api/v1/marketplace/plugins/search',
@@ -43,6 +44,7 @@ export class CloudServiceClient extends BaseHttpClient {
page_size,
sort_by,
sort_order,
component_filter,
},
);
}

View File

@@ -8,32 +8,40 @@ import { cn } from '@/lib/utils';
import { toggleVariants } from '@/components/ui/toggle';
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
VariantProps<typeof toggleVariants> & {
spacing?: number;
}
>({
size: 'default',
variant: 'default',
spacing: 0,
});
function ToggleGroup({
className,
variant,
size,
spacing = 0,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
VariantProps<typeof toggleVariants> & {
spacing?: number;
}) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ '--gap': spacing } as React.CSSProperties}
className={cn(
'group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs',
'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs',
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
@@ -55,12 +63,14 @@ function ToggleGroupItem({
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10',
'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l',
className,
)}
{...props}

View File

@@ -7,7 +7,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:data-[state=on]:bg-slate-700 dark:data-[state=on]:text-white [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {

View File

@@ -351,6 +351,8 @@ const enUS = {
markAsRead: 'Mark as Read',
markAsReadSuccess: 'Marked as read',
markAsReadFailed: 'Mark as read failed',
filterByComponent: 'Component',
allComponents: 'All Components',
},
mcp: {
title: 'MCP',

View File

@@ -353,6 +353,8 @@ const jaJP = {
markAsRead: '既読',
markAsReadSuccess: '既読に設定しました',
markAsReadFailed: '既読に設定に失敗しました',
filterByComponent: 'コンポーネント',
allComponents: '全部コンポーネント',
},
mcp: {
title: 'MCP',

View File

@@ -335,6 +335,8 @@ const zhHans = {
markAsRead: '已读',
markAsReadSuccess: '已标记为已读',
markAsReadFailed: '标记为已读失败',
filterByComponent: '组件',
allComponents: '全部组件',
},
mcp: {
title: 'MCP',

View File

@@ -333,6 +333,8 @@ const zhHant = {
markAsRead: '已讀',
markAsReadSuccess: '已標記為已讀',
markAsReadFailed: '標記為已讀失敗',
filterByComponent: '組件',
allComponents: '全部組件',
},
mcp: {
title: 'MCP',