diff --git a/pkg/plugin/manager.py b/pkg/plugin/manager.py index f813d2e2..bf2027f4 100644 --- a/pkg/plugin/manager.py +++ b/pkg/plugin/manager.py @@ -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) diff --git a/web/package-lock.json b/web/package-lock.json index be9e91fb..410bc379 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index 88b493a5..ee33ccc5 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 4be10db3..e9be58c3 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -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.WAIT_INPUT); const [installError, setInstallError] = useState(null); @@ -90,6 +93,15 @@ export default function PluginConfigPage() {
+
); } diff --git a/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx b/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx new file mode 100644 index 00000000..b712c374 --- /dev/null +++ b/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx @@ -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 ( +
+
+
+ {plugin.author} +
+
{plugin.name}
+
+ {plugin.description} +
+
+
+ ); +} + +export default function PluginSortDialog({ + open, + onOpenChange, + onSortComplete, +}: PluginSortDialogProps) { + const [sortedPlugins, setSortedPlugins] = useState([]); + 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 ( + + + + 插件排序 + +
+

+ 插件顺序会影响同一事件内的处理顺序,请拖动插件卡片排序 +

+ + `${plugin.author}-${plugin.name}`, + )} + strategy={verticalListSortingStrategy} + > + {sortedPlugins.map((plugin) => ( + + ))} + + +
+ + + + +
+
+ ); +} diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 2c328c42..33c49aa6 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -375,7 +375,7 @@ class HttpClient { } public reorderPlugins(plugins: PluginReorderElement[]): Promise { - 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'); diff --git a/web/src/components/ui/hover-card.tsx b/web/src/components/ui/hover-card.tsx index dc543a33..20ca3a4d 100644 --- a/web/src/components/ui/hover-card.tsx +++ b/web/src/components/ui/hover-card.tsx @@ -8,14 +8,7 @@ import { cn } from '@/lib/utils'; function HoverCard({ ...props }: React.ComponentProps) { - return ( - - ); + return ; } 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}