import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { getErrorMessage, useUpdatePluginMutation } from "@/lib/store"; import { Plugin } from "@/lib/types/plugins"; import { cn } from "@/lib/utils"; import { DragDropProvider } from "@dnd-kit/react"; import { useSortable } from "@dnd-kit/react/sortable"; import { GripVertical, Lock } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; const BUILTIN_ID = "__builtin__"; interface SequenceItem { id: string; type: "builtin" | "custom"; plugin?: Plugin; } interface PluginSequenceSheetProps { open: boolean; onClose: () => void; plugins: Plugin[]; } function buildSequenceItems(plugins: Plugin[]): SequenceItem[] { const customPlugins = plugins.filter((p) => p.isCustom); const preBuiltin = customPlugins.filter((p) => p.placement === "pre_builtin").sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); const postBuiltin = customPlugins.filter((p) => p.placement !== "pre_builtin").sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); return [ ...preBuiltin.map((p) => ({ id: p.name, type: "custom" as const, plugin: p })), { id: BUILTIN_ID, type: "builtin" as const }, ...postBuiltin.map((p) => ({ id: p.name, type: "custom" as const, plugin: p })), ]; } function SortableBlock({ item, index }: { item: SequenceItem; index: number }) { const isBuiltin = item.type === "builtin"; const { ref, isDragging, handleRef, targetRef } = useSortable({ id: item.id, index, }); return (
{isBuiltin ? ( ) : (
)} {isBuiltin ? "Built-in Plugins" : item.plugin?.name} {!isBuiltin && item.plugin?.status && (
)}
); } export default function PluginSequenceSheet({ open, onClose, plugins }: PluginSequenceSheetProps) { const [items, setItems] = useState([]); const [updatePlugin, { isLoading }] = useUpdatePluginMutation(); const wasOpenRef = useRef(false); useEffect(() => { if (open && !wasOpenRef.current) { setItems(buildSequenceItems(plugins)); } wasOpenRef.current = open; }, [open, plugins]); const handleSave = useCallback(async () => { const builtinIndex = items.findIndex((item) => item.type === "builtin"); if (builtinIndex === -1) return; const updates: { name: string; placement: string; order: number }[] = []; items.forEach((item, index) => { if (item.type !== "custom" || !item.plugin) return; const placement = index < builtinIndex ? "pre_builtin" : "post_builtin"; const groupItems = items.filter((it, i) => it.type === "custom" && (index < builtinIndex ? i < builtinIndex : i > builtinIndex)); const order = groupItems.findIndex((it) => it.id === item.id); if (item.plugin.placement !== placement || item.plugin.order !== order) { updates.push({ name: item.plugin.name, placement, order }); } }); if (updates.length === 0) { onClose(); return; } try { for (const u of updates) { const plugin = items.find((i) => i.id === u.name)?.plugin; await updatePlugin({ name: u.name, data: { enabled: plugin?.enabled ?? true, config: plugin?.config, path: plugin?.path, placement: u.placement, order: u.order, }, }).unwrap(); } toast.success("Plugin sequence updated"); onClose(); } catch (error) { toast.error(getErrorMessage(error)); } }, [items, updatePlugin, onClose]); return ( Edit Plugin Sequence Drag plugins above or below the built-in plugins block to control execution order.
{ const { source, target } = event.operation; if (!source || !target || source.id === target.id) return; setItems((current) => { const sourceIndex = current.findIndex((item) => item.id === source.id); const targetIndex = current.findIndex((item) => item.id === target.id); if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) return current; const newItems = [...current]; const [movedItem] = newItems.splice(sourceIndex, 1); newItems.splice(targetIndex, 0, movedItem); return newItems; }); }} > {items.map((item, index) => ( ))}
If your config.json file has plugin sequence configured, it will take precedence over the sequence configured in the UI after restarting Bifrost.
); }