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:
devin-ai-integration[bot]
2025-05-13 14:10:18 +08:00
committed by GitHub
parent ae6979151f
commit 86ff6f5eb6
7 changed files with 290 additions and 14 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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');

View File

@@ -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}