mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
feat: plugin reordering (#1398)
* Add @dnd-kit/core and @dnd-kit/sortable dependencies for plugin sorting Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * Add PluginSortDialog component with drag-and-drop functionality Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * Integrate sorting button and dialog into PluginInstalledComponent Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * Update HttpClient to use local backend URL for development Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * Fix reorderPlugins method to use PUT and correct request format Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * Update hover-card component using shadcn CLI Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * Fix formatting issues in plugin sorting components Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * refactor: move plugin sorting button and dialog to page component Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * refactor: move PluginSortDialog component to plugins directory Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * chore: remove old PluginSortDialog component file Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * fix: api bug * perf: desciption in plugin sorting dialog * fix: lint errors --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ae6979151f
commit
86ff6f5eb6
@@ -79,7 +79,7 @@ class PluginManager:
|
||||
await self.load_plugin_settings(self.plugin_containers)
|
||||
|
||||
# 按优先级倒序
|
||||
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
|
||||
self.plugin_containers.sort(key=lambda x: x.priority, reverse=False)
|
||||
|
||||
self.ap.logger.debug(f'优先级排序后的插件列表 {self.plugin_containers}')
|
||||
|
||||
@@ -295,7 +295,7 @@ class PluginManager:
|
||||
plugin.priority = plugin_priority
|
||||
break
|
||||
|
||||
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
|
||||
self.plugin_containers.sort(key=lambda x: x.priority, reverse=False)
|
||||
|
||||
for plugin in self.plugin_containers:
|
||||
await self.dump_plugin_container_setting(plugin)
|
||||
|
||||
55
web/package-lock.json
generated
55
web/package-lock.json
generated
@@ -8,6 +8,8 @@
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
@@ -66,6 +68,59 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
|
||||
@@ -3,6 +3,7 @@ import PluginInstalledComponent, {
|
||||
PluginInstalledComponentRef,
|
||||
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent';
|
||||
import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent';
|
||||
import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
|
||||
import styles from './plugins.module.css';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -19,6 +20,7 @@ import { GithubIcon } from 'lucide-react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
enum PluginInstallStatus {
|
||||
WAIT_INPUT = 'wait_input',
|
||||
INSTALLING = 'installing',
|
||||
@@ -27,6 +29,7 @@ enum PluginInstallStatus {
|
||||
|
||||
export default function PluginConfigPage() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [sortModalOpen, setSortModalOpen] = useState(false);
|
||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
@@ -90,6 +93,15 @@ export default function PluginConfigPage() {
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row justify-end items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="px-6 py-4 cursor-pointer mr-2"
|
||||
onClick={() => {
|
||||
setSortModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编排
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
className="px-6 py-4 cursor-pointer"
|
||||
@@ -166,6 +178,14 @@ export default function PluginConfigPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<PluginSortDialog
|
||||
open={sortModalOpen}
|
||||
onOpenChange={setSortModalOpen}
|
||||
onSortComplete={() => {
|
||||
pluginInstalledRef.current?.refreshPluginList();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
206
web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx
Normal file
206
web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { PluginReorderElement } from '@/app/infra/entities/api';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
interface PluginSortDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSortComplete: () => void;
|
||||
}
|
||||
|
||||
function SortablePluginItem({ plugin }: { plugin: PluginCardVO }) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({
|
||||
id: `${plugin.author}-${plugin.name}`,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="bg-white dark:bg-gray-800 p-4 rounded-md shadow-sm border mb-2 cursor-move"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{plugin.author}
|
||||
</div>
|
||||
<div className="text-lg font-medium">{plugin.name}</div>
|
||||
<div className="text-sm line-clamp-2 text-gray-500 dark:text-gray-400 mt-1">
|
||||
{plugin.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PluginSortDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSortComplete,
|
||||
}: PluginSortDialogProps) {
|
||||
const [sortedPlugins, setSortedPlugins] = useState<PluginCardVO[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
function getPluginList() {
|
||||
httpClient.getPlugins().then((value) => {
|
||||
setSortedPlugins(
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.author,
|
||||
description: plugin.description.zh_CN,
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
status: plugin.status,
|
||||
tools: plugin.tools,
|
||||
event_handlers: plugin.event_handlers,
|
||||
repository: plugin.repository,
|
||||
priority: plugin.priority,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
getPluginList();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
console.log('Drag end event:', { active, over });
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
setSortedPlugins((items) => {
|
||||
const oldIndex = items.findIndex(
|
||||
(item) => `${item.author}-${item.name}` === active.id,
|
||||
);
|
||||
const newIndex = items.findIndex(
|
||||
(item) => `${item.author}-${item.name}` === over.id,
|
||||
);
|
||||
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
|
||||
return newItems;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
setIsLoading(true);
|
||||
|
||||
const reorderElements: PluginReorderElement[] = sortedPlugins.map(
|
||||
(plugin, index) => ({
|
||||
author: plugin.author,
|
||||
name: plugin.name,
|
||||
priority: index,
|
||||
}),
|
||||
);
|
||||
|
||||
httpClient
|
||||
.reorderPlugins(reorderElements)
|
||||
.then(() => {
|
||||
toast.success('插件排序成功');
|
||||
onSortComplete();
|
||||
onOpenChange(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('排序失败:' + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-0">
|
||||
<DialogTitle>插件排序</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-0">
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
插件顺序会影响同一事件内的处理顺序,请拖动插件卡片排序
|
||||
</p>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={sortedPlugins.map(
|
||||
(plugin) => `${plugin.author}-${plugin.name}`,
|
||||
)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{sortedPlugins.map((plugin) => (
|
||||
<SortablePluginItem
|
||||
key={`${plugin.author}-${plugin.name}`}
|
||||
plugin={plugin}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
<DialogFooter className="px-6 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isLoading}>
|
||||
{isLoading ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -375,7 +375,7 @@ class HttpClient {
|
||||
}
|
||||
|
||||
public reorderPlugins(plugins: PluginReorderElement[]): Promise<object> {
|
||||
return this.post('/api/v1/plugins/reorder', plugins);
|
||||
return this.put('/api/v1/plugins/reorder', { plugins });
|
||||
}
|
||||
|
||||
public updatePlugin(
|
||||
@@ -445,8 +445,8 @@ class HttpClient {
|
||||
}
|
||||
|
||||
// export const httpClient = new HttpClient("https://version-4.langbot.dev");
|
||||
// export const httpClient = new HttpClient('http://localhost:5300');
|
||||
export const httpClient = new HttpClient('/');
|
||||
export const httpClient = new HttpClient('http://localhost:5300');
|
||||
// export const httpClient = new HttpClient('/');
|
||||
|
||||
// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法
|
||||
export const spaceClient = new HttpClient('https://space.langbot.app');
|
||||
|
||||
@@ -8,14 +8,7 @@ import { cn } from '@/lib/utils';
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Root
|
||||
openDelay={0}
|
||||
closeDelay={0}
|
||||
data-slot="hover-card"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
@@ -39,7 +32,7 @@ function HoverCardContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden [animation-duration:50ms]',
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user