mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-18 10:26:07 +00:00
feat: add component filter to marketplace page
This commit is contained in:
@@ -10,7 +10,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} 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 PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
|
||||||
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
||||||
import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog';
|
import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog';
|
||||||
@@ -38,6 +39,7 @@ function MarketPageContent({
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [componentFilter, setComponentFilter] = useState<string>('all');
|
||||||
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
|
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
@@ -127,23 +129,18 @@ function MarketPageContent({
|
|||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
const { sortBy, sortOrder } = getCurrentSort();
|
const { sortBy, sortOrder } = getCurrentSort();
|
||||||
|
const filterValue =
|
||||||
|
componentFilter === 'all' ? undefined : componentFilter;
|
||||||
|
|
||||||
if (isSearch && searchQuery.trim()) {
|
// Always use searchMarketplacePlugins to support component filtering
|
||||||
response = await getCloudServiceClientSync().searchMarketplacePlugins(
|
response = await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
searchQuery.trim(),
|
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
);
|
filterValue,
|
||||||
} else {
|
);
|
||||||
response = await getCloudServiceClientSync().getMarketplacePlugins(
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sortBy,
|
|
||||||
sortOrder,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: ApiRespMarketplacePlugins = response;
|
const data: ApiRespMarketplacePlugins = response;
|
||||||
const newPlugins = data.plugins.map(transformToVO);
|
const newPlugins = data.plugins.map(transformToVO);
|
||||||
@@ -168,7 +165,14 @@ function MarketPageContent({
|
|||||||
setIsLoadingMore(false);
|
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
|
// 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(() => {
|
useEffect(() => {
|
||||||
fetchPlugins(1, !!searchQuery.trim(), true);
|
fetchPlugins(1, !!searchQuery.trim(), true);
|
||||||
}, [sortOption]);
|
}, [sortOption, componentFilter]);
|
||||||
|
|
||||||
// 处理URL参数,检查是否需要打开插件详情对话框
|
// 处理URL参数,检查是否需要打开插件详情对话框
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -343,9 +355,59 @@ function MarketPageContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sort dropdown */}
|
{/* Component filter and sort */}
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-3 sm:px-4">
|
||||||
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
{/* 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">
|
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||||
{t('market.sortBy')}:
|
{t('market.sortBy')}:
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
page_size: number,
|
page_size: number,
|
||||||
sort_by?: string,
|
sort_by?: string,
|
||||||
sort_order?: string,
|
sort_order?: string,
|
||||||
|
component_filter?: string,
|
||||||
): Promise<ApiRespMarketplacePlugins> {
|
): Promise<ApiRespMarketplacePlugins> {
|
||||||
return this.post<ApiRespMarketplacePlugins>(
|
return this.post<ApiRespMarketplacePlugins>(
|
||||||
'/api/v1/marketplace/plugins/search',
|
'/api/v1/marketplace/plugins/search',
|
||||||
@@ -43,6 +44,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
page_size,
|
page_size,
|
||||||
sort_by,
|
sort_by,
|
||||||
sort_order,
|
sort_order,
|
||||||
|
component_filter,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,40 @@ import { cn } from '@/lib/utils';
|
|||||||
import { toggleVariants } from '@/components/ui/toggle';
|
import { toggleVariants } from '@/components/ui/toggle';
|
||||||
|
|
||||||
const ToggleGroupContext = React.createContext<
|
const ToggleGroupContext = React.createContext<
|
||||||
VariantProps<typeof toggleVariants>
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}
|
||||||
>({
|
>({
|
||||||
size: 'default',
|
size: 'default',
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
|
spacing: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
function ToggleGroup({
|
function ToggleGroup({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
|
spacing = 0,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
VariantProps<typeof toggleVariants>) {
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<ToggleGroupPrimitive.Root
|
<ToggleGroupPrimitive.Root
|
||||||
data-slot="toggle-group"
|
data-slot="toggle-group"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
|
data-spacing={spacing}
|
||||||
|
style={{ '--gap': spacing } as React.CSSProperties}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||||
{children}
|
{children}
|
||||||
</ToggleGroupContext.Provider>
|
</ToggleGroupContext.Provider>
|
||||||
</ToggleGroupPrimitive.Root>
|
</ToggleGroupPrimitive.Root>
|
||||||
@@ -55,12 +63,14 @@ function ToggleGroupItem({
|
|||||||
data-slot="toggle-group-item"
|
data-slot="toggle-group-item"
|
||||||
data-variant={context.variant || variant}
|
data-variant={context.variant || variant}
|
||||||
data-size={context.size || size}
|
data-size={context.size || size}
|
||||||
|
data-spacing={context.spacing}
|
||||||
className={cn(
|
className={cn(
|
||||||
toggleVariants({
|
toggleVariants({
|
||||||
variant: context.variant || variant,
|
variant: context.variant || variant,
|
||||||
size: context.size || size,
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const toggleVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -351,6 +351,8 @@ const enUS = {
|
|||||||
markAsRead: 'Mark as Read',
|
markAsRead: 'Mark as Read',
|
||||||
markAsReadSuccess: 'Marked as read',
|
markAsReadSuccess: 'Marked as read',
|
||||||
markAsReadFailed: 'Mark as read failed',
|
markAsReadFailed: 'Mark as read failed',
|
||||||
|
filterByComponent: 'Component',
|
||||||
|
allComponents: 'All Components',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
|
|||||||
@@ -353,6 +353,8 @@ const jaJP = {
|
|||||||
markAsRead: '既読',
|
markAsRead: '既読',
|
||||||
markAsReadSuccess: '既読に設定しました',
|
markAsReadSuccess: '既読に設定しました',
|
||||||
markAsReadFailed: '既読に設定に失敗しました',
|
markAsReadFailed: '既読に設定に失敗しました',
|
||||||
|
filterByComponent: 'コンポーネント',
|
||||||
|
allComponents: '全部コンポーネント',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ const zhHans = {
|
|||||||
markAsRead: '已读',
|
markAsRead: '已读',
|
||||||
markAsReadSuccess: '已标记为已读',
|
markAsReadSuccess: '已标记为已读',
|
||||||
markAsReadFailed: '标记为已读失败',
|
markAsReadFailed: '标记为已读失败',
|
||||||
|
filterByComponent: '组件',
|
||||||
|
allComponents: '全部组件',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ const zhHant = {
|
|||||||
markAsRead: '已讀',
|
markAsRead: '已讀',
|
||||||
markAsReadSuccess: '已標記為已讀',
|
markAsReadSuccess: '已標記為已讀',
|
||||||
markAsReadFailed: '標記為已讀失敗',
|
markAsReadFailed: '標記為已讀失敗',
|
||||||
|
filterByComponent: '組件',
|
||||||
|
allComponents: '全部組件',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
|
|||||||
Reference in New Issue
Block a user