import { useCallback, useEffect, useMemo, useState, type DragEvent } from "react"; import { Button } from "@heroui/react"; import type { ModuleDefinition } from "../../../config/modules"; import { useI18n } from "../../../i18n"; import { useToast } from "../../../hooks/useToast"; import { openOrFocusModule } from "../../../utils/moduleWindows"; import { useWorkbenchLayout } from "../useWorkbenchLayout"; import { ConsoleIcon } from "./ConsoleIcon"; import { WorkbenchCardIcon } from "./WorkbenchCardIcon"; type Props = { /** Permission-filtered customizable modules (monitoring + operations). */ opsCandidates: ModuleDefinition[]; systemMods: ModuleDefinition[]; }; function draftFromLayout(shown: ModuleDefinition[], available: ModuleDefinition[]) { return { order: shown.map((m) => m.moduleId), hidden: available.map((m) => m.moduleId), }; } /** Primary modules as a grid of tiles, system admin as a quiet link row. */ export function WorkbenchModules({ opsCandidates, systemMods }: Props) { const { t } = useI18n(); const { showError } = useToast(); const { shown, available, save, reset, layoutPrefs } = useWorkbenchLayout(opsCandidates); const [editing, setEditing] = useState(false); const [draftOrder, setDraftOrder] = useState([]); const [draftHidden, setDraftHidden] = useState([]); const [dragId, setDragId] = useState(null); const [saving, setSaving] = useState(false); const byId = useMemo(() => new Map(opsCandidates.map((m) => [m.moduleId, m])), [opsCandidates]); const enterEdit = useCallback(() => { const d = draftFromLayout(shown, available); setDraftOrder(d.order); setDraftHidden(d.hidden); setEditing(true); }, [shown, available]); const exitEdit = useCallback(() => { setEditing(false); setDragId(null); setDraftOrder([]); setDraftHidden([]); }, []); useEffect(() => { if (!editing) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); exitEdit(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [editing, exitEdit]); const draftShown = useMemo( () => draftOrder.map((id) => byId.get(id)).filter((m): m is ModuleDefinition => Boolean(m)), [draftOrder, byId], ); const draftAvailable = useMemo( () => draftHidden.map((id) => byId.get(id)).filter((m): m is ModuleDefinition => Boolean(m)), [draftHidden, byId], ); const displayOps = editing ? draftShown : shown; const canCustomize = opsCandidates.length > 0; const removeTile = (moduleId: string) => { setDraftOrder((prev) => prev.filter((id) => id !== moduleId)); setDraftHidden((prev) => (prev.includes(moduleId) ? prev : [...prev, moduleId])); }; const addTile = (moduleId: string) => { setDraftHidden((prev) => prev.filter((id) => id !== moduleId)); setDraftOrder((prev) => (prev.includes(moduleId) ? prev : [...prev, moduleId])); }; const onDragStart = (moduleId: string) => (e: DragEvent) => { setDragId(moduleId); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", moduleId); }; const onDragOver = (overId: string) => (e: DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (!dragId || dragId === overId) return; setDraftOrder((prev) => { const from = prev.indexOf(dragId); const to = prev.indexOf(overId); if (from < 0 || to < 0 || from === to) return prev; const next = [...prev]; next.splice(from, 1); next.splice(to, 0, dragId); return next; }); }; const onDrop = (e: DragEvent) => { e.preventDefault(); setDragId(null); }; const defaults = useMemo( () => ({ order: opsCandidates.filter((m) => !m.defaultHidden).map((m) => m.moduleId), hidden: opsCandidates.filter((m) => m.defaultHidden).map((m) => m.moduleId), }), [opsCandidates], ); const onDone = async () => { setSaving(true); try { const sameAsDefault = draftOrder.join(",") === defaults.order.join(",") && [...draftHidden].sort().join(",") === [...defaults.hidden].sort().join(","); if (sameAsDefault) { // Store nothing so later registry changes (new modules, reordering) still apply. await reset(); } else { // Keep entries for modules this user can't see right now (e.g. scope temporarily // revoked) so their choice survives until the permission comes back. const keepOrder = (layoutPrefs?.order ?? []).filter((id) => !byId.has(id)); const keepHidden = (layoutPrefs?.hidden ?? []).filter((id) => !byId.has(id)); await save([...draftOrder, ...keepOrder], [...draftHidden, ...keepHidden]); } exitEdit(); } catch { showError(t("workbench.customizeSaveFailed")); } finally { setSaving(false); } }; const onReset = () => { setDraftOrder(defaults.order); setDraftHidden(defaults.hidden); }; return (
{canCustomize ? (
{!editing ? ( ) : null}
) : null} {canCustomize || displayOps.length > 0 ? (
{displayOps.length === 0 && !editing ? (
) : ( displayOps.map((mod) => (
setDragId(null)} > {editing ? ( ) : null}
)) )}
) : null} {editing && draftAvailable.length > 0 ? (
{t("workbench.customizeAvailable")}
{draftAvailable.map((mod) => ( ))}
) : null} {editing ? (
) : null} {systemMods.length > 0 ? (
{systemMods.map((mod) => ( ))}
) : null}
); }