Files
drift/src/components/sidebar/Entity.tsx
T
2026-02-28 14:24:18 +01:00

265 lines
6.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { ChevronDown } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "../ui/sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "../ui/context-menu";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Route, Workspace, LayoutNode } from "@/lib/types";
type DialogMode = "rename" | "add-item" | "add-group";
type Props = {
node: LayoutNode;
workspace: Workspace;
setRoute: (route: Route) => void;
addEntity: (
type: "todo" | "doc",
parentGroupId: string | null,
name?: string,
) => string | undefined;
addGroup: (name: string, parentGroupId: string | null) => string;
renameEntity: (entityId: string, newName: string) => void;
deleteEntity: (entityId: string) => void;
removeNode: (id: string) => void;
renameGroup: (id: string, name: string) => void;
};
export default function EntitySwitch(props: Props) {
const {
node,
workspace,
setRoute,
addEntity,
addGroup,
renameEntity,
deleteEntity,
removeNode,
renameGroup,
} = props;
const [dialogValue, setDialogValue] = useState("");
const [dialogMode, setDialogMode] = useState<DialogMode | undefined>();
const parentId = node.type === "group" ? node.id : null;
const openDialog = (mode: DialogMode, initial = "") => {
setDialogMode(mode);
setDialogValue(
initial ||
(mode === "add-item"
? "New list"
: mode === "add-group"
? "New group"
: ""),
);
};
const closeDialog = () => setDialogMode(undefined);
const handleSubmit = () => {
const value = dialogValue.trim();
if (!value) return;
switch (dialogMode) {
case "rename":
if (node.type === "group") renameGroup(node.id, value);
else if (node.type === "item") renameEntity(node.entityId, value);
break;
case "add-item":
addEntity("todo", parentId, value);
break;
case "add-group":
addGroup(value, parentId);
break;
}
closeDialog();
};
const confirmDelete = (message: string, action: () => void) => {
if (window.confirm(message)) action();
};
const dialogTitle = dialogMode?.startsWith("rename")
? "Rename"
: dialogMode === "add-item"
? "Add new list"
: "Add new group";
const dialog = (
<Dialog open={!!dialogMode} onOpenChange={(o) => !o && closeDialog()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{dialogTitle}</DialogTitle>
{(dialogMode === "add-item" || dialogMode === "add-group") && (
<DialogDescription>Enter a name below.</DialogDescription>
)}
</DialogHeader>
<Input
autoFocus
value={dialogValue}
onChange={(e) => setDialogValue(e.target.value)}
/>
<DialogFooter>
<Button variant="outline" onClick={closeDialog}>
Cancel
</Button>
<Button onClick={handleSubmit}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
/* ================= GROUP ================= */
if (node.type === "group") {
return (
<>
<Collapsible defaultOpen className="group/collapsible">
<SidebarGroup>
<SidebarGroupLabel asChild>
<ContextMenu>
<ContextMenuTrigger asChild>
<CollapsibleTrigger asChild>
<SidebarMenuButton className="w-full">
{node.name}
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={() => openDialog("add-item")}>
Add list
</ContextMenuItem>
<ContextMenuItem onSelect={() => openDialog("add-group")}>
Add group
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
onSelect={() => openDialog("rename", node.name)}
>
Rename
</ContextMenuItem>
<ContextMenuItem
variant="destructive"
onSelect={() =>
confirmDelete("Delete this group and its children?", () =>
removeNode(node.id),
)
}
>
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{node.children.map((child) => (
<EntitySwitch
key={child.type === "group" ? child.id : child.entityId}
{...props}
node={child}
/>
))}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
{dialog}
</>
);
}
/* ================= ITEM ================= */
const entity = workspace.entities[node.entityId];
if (!entity) return null;
return (
<>
<SidebarMenuItem>
<ContextMenu>
<ContextMenuTrigger asChild>
<SidebarMenuButton
onClick={() =>
setRoute({
workspace: workspace.id,
entity: node.entityId,
})
}
>
{entity.name}
</SidebarMenuButton>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={() => openDialog("rename", entity.name)}>
Rename
</ContextMenuItem>
<ContextMenuItem
variant="destructive"
onSelect={() =>
confirmDelete("Delete this list?", () =>
deleteEntity(node.entityId),
)
}
>
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</SidebarMenuItem>
{dialog}
</>
);
}