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