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

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