From e48672cf7741e5be83b6e05cb1e82c418f6ef345 Mon Sep 17 00:00:00 2001 From: Linus Rath Date: Thu, 26 Mar 2026 10:30:48 +0100 Subject: [PATCH] feat: implement drag-and-drop functionality for sidebar apps #101 --- components/settings/sidebar-apps-settings.tsx | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/components/settings/sidebar-apps-settings.tsx b/components/settings/sidebar-apps-settings.tsx index 61478a0c..4045a211 100644 --- a/components/settings/sidebar-apps-settings.tsx +++ b/components/settings/sidebar-apps-settings.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef } from "react"; import { useTranslations } from "next-intl"; import { Plus, Pencil, Trash2, ExternalLink, PanelRight, GripVertical } from "lucide-react"; import { icons as lucideIcons, type LucideIcon } from "lucide-react"; @@ -180,10 +180,12 @@ function AppForm({ export function SidebarAppsSettings() { const t = useTranslations("settings.sidebar_apps"); const tApps = useTranslations("sidebar_apps"); - const { sidebarApps, keepAppsLoaded, addSidebarApp, updateSidebarApp, removeSidebarApp, updateSetting } = useSettingsStore(); + const { sidebarApps, keepAppsLoaded, addSidebarApp, updateSidebarApp, removeSidebarApp, reorderSidebarApps, updateSetting } = useSettingsStore(); const [editingApp, setEditingApp] = useState(null); const [showAddForm, setShowAddForm] = useState(false); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); + const [dragOverIndex, setDragOverIndex] = useState(null); + const draggedIndexRef = useRef(null); const handleAdd = useCallback((data: SidebarAppFormData) => { const id = `app-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; @@ -207,6 +209,34 @@ export function SidebarAppsSettings() { removeSidebarApp(app.id); }, [confirmDialog, tApps, removeSidebarApp]); + const handleDragStart = useCallback((e: React.DragEvent, index: number) => { + draggedIndexRef.current = index; + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", String(index)); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent, index: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setDragOverIndex(index); + }, []); + + const handleDrop = useCallback((e: React.DragEvent, dropIndex: number) => { + e.preventDefault(); + setDragOverIndex(null); + const fromIndex = draggedIndexRef.current; + if (fromIndex === null || fromIndex === dropIndex) return; + const newApps = [...sidebarApps]; + const [moved] = newApps.splice(fromIndex, 1); + newApps.splice(dropIndex, 0, moved); + reorderSidebarApps(newApps); + }, [sidebarApps, reorderSidebarApps]); + + const handleDragEnd = useCallback(() => { + draggedIndexRef.current = null; + setDragOverIndex(null); + }, []); + return ( <> @@ -224,7 +254,7 @@ export function SidebarAppsSettings() {

{tApps("no_apps_hint")}

)} - {sidebarApps.map((app) => { + {sidebarApps.map((app, index) => { if (editingApp === app.id) { return ( handleDragStart(e, index)} + onDragOver={(e) => handleDragOver(e, index)} + onDrop={(e) => handleDrop(e, index)} + onDragEnd={handleDragEnd} + className={cn( + "flex items-center gap-3 p-3 border rounded-lg transition-colors", + dragOverIndex === index + ? "border-primary bg-primary/5" + : "border-border hover:bg-muted/50" + )} > - +
+ +
{AppIcon ? : null}