feat: add context menu functionality to DirectoryNode for enhanced user interaction
This commit is contained in:
@@ -18,6 +18,12 @@ import { toast } from "sonner";
|
|||||||
import { OptionCombobox } from "@/components/option-combobox";
|
import { OptionCombobox } from "@/components/option-combobox";
|
||||||
import { ProjectDialog } from "@/components/project-dialog";
|
import { ProjectDialog } from "@/components/project-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "@/components/ui/context-menu";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -80,6 +86,7 @@ export function KnowledgeDirectoryPanel({
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [directories, setDirectories] = useState<KnowledgeDirectory[]>([]);
|
const [directories, setDirectories] = useState<KnowledgeDirectory[]>([]);
|
||||||
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
|
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
|
||||||
|
const [contextMenuDirectoryId, setContextMenuDirectoryId] = useState<number | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [dialog, setDialog] = useState<DirectoryDialogState>({
|
const [dialog, setDialog] = useState<DirectoryDialogState>({
|
||||||
@@ -230,9 +237,13 @@ export function KnowledgeDirectoryPanel({
|
|||||||
depth={0}
|
depth={0}
|
||||||
expandedIds={expandedIds}
|
expandedIds={expandedIds}
|
||||||
selectedDirectoryId={selectedDirectoryId}
|
selectedDirectoryId={selectedDirectoryId}
|
||||||
|
contextMenuDirectoryId={contextMenuDirectoryId}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onToggle={toggleDirectory}
|
onToggle={toggleDirectory}
|
||||||
onSelect={onSelectDirectory}
|
onSelect={onSelectDirectory}
|
||||||
|
onContextMenuOpenChange={(directoryId, open) =>
|
||||||
|
setContextMenuDirectoryId(open ? directoryId : null)
|
||||||
|
}
|
||||||
onCreate={openCreate}
|
onCreate={openCreate}
|
||||||
onEdit={openEdit}
|
onEdit={openEdit}
|
||||||
onDelete={(directory) => void handleDelete(directory)}
|
onDelete={(directory) => void handleDelete(directory)}
|
||||||
@@ -339,9 +350,11 @@ type DirectoryNodeProps = {
|
|||||||
depth: number;
|
depth: number;
|
||||||
expandedIds: Set<number>;
|
expandedIds: Set<number>;
|
||||||
selectedDirectoryId: number | null;
|
selectedDirectoryId: number | null;
|
||||||
|
contextMenuDirectoryId: number | null;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
onToggle: (id: number) => void;
|
onToggle: (id: number) => void;
|
||||||
onSelect: (id: number) => void;
|
onSelect: (id: number) => void;
|
||||||
|
onContextMenuOpenChange: (id: number, open: boolean) => void;
|
||||||
onCreate: (parentId: number) => void;
|
onCreate: (parentId: number) => void;
|
||||||
onEdit: (item: KnowledgeDirectory) => void;
|
onEdit: (item: KnowledgeDirectory) => void;
|
||||||
onDelete: (item: KnowledgeDirectory) => void;
|
onDelete: (item: KnowledgeDirectory) => void;
|
||||||
@@ -355,9 +368,11 @@ function DirectoryNode({
|
|||||||
depth,
|
depth,
|
||||||
expandedIds,
|
expandedIds,
|
||||||
selectedDirectoryId,
|
selectedDirectoryId,
|
||||||
|
contextMenuDirectoryId,
|
||||||
saving,
|
saving,
|
||||||
onToggle,
|
onToggle,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onContextMenuOpenChange,
|
||||||
onCreate,
|
onCreate,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -365,69 +380,93 @@ function DirectoryNode({
|
|||||||
}: DirectoryNodeProps) {
|
}: DirectoryNodeProps) {
|
||||||
const expanded = expandedIds.has(item.id);
|
const expanded = expandedIds.has(item.id);
|
||||||
const hasChildren = (item.children || []).length > 0;
|
const hasChildren = (item.children || []).length > 0;
|
||||||
|
const active = selectedDirectoryId === item.id || contextMenuDirectoryId === item.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<ContextMenu onOpenChange={(open) => onContextMenuOpenChange(item.id, open)}>
|
||||||
className={cn(
|
<ContextMenuTrigger className="block">
|
||||||
"group flex items-center gap-1 px-2 py-1.5 text-sm hover:bg-accent",
|
<div
|
||||||
selectedDirectoryId === item.id && "bg-accent text-accent-foreground",
|
className={cn(
|
||||||
)}
|
"group flex items-center gap-1 px-2 py-1.5 text-sm hover:bg-accent",
|
||||||
style={{ paddingLeft: 8 + depth * 16 }}
|
active && "bg-accent text-accent-foreground",
|
||||||
>
|
)}
|
||||||
<Button
|
style={{ paddingLeft: 8 + depth * 16 }}
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-5 shrink-0"
|
|
||||||
disabled={!hasChildren}
|
|
||||||
onClick={() => onToggle(item.id)}
|
|
||||||
aria-label={expanded ? t("knowledge.collapseDirectory") : t("knowledge.expandDirectory")}
|
|
||||||
>
|
|
||||||
{expanded ? <ChevronDownIcon className="size-3.5" /> : <ChevronRightIcon className="size-3.5" />}
|
|
||||||
</Button>
|
|
||||||
<FolderIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="min-w-0 flex-1 truncate text-left"
|
|
||||||
onClick={() => onSelect(item.id)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-6 opacity-0 group-hover:opacity-100"
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
aria-label={t("knowledge.moreActions", { name: item.name })}
|
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-3.5" />
|
<Button
|
||||||
</DropdownMenuTrigger>
|
variant="ghost"
|
||||||
<DropdownMenuContent align="end" className="w-40 min-w-40">
|
size="icon"
|
||||||
{item.parentId === 0 ? (
|
className="size-5 shrink-0"
|
||||||
<DropdownMenuItem onClick={() => onCreate(item.id)}>
|
disabled={!hasChildren}
|
||||||
<PlusIcon className="mr-2 size-3.5" />
|
onClick={() => onToggle(item.id)}
|
||||||
{t("knowledge.createSubDirectory")}
|
aria-label={expanded ? t("knowledge.collapseDirectory") : t("knowledge.expandDirectory")}
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
<DropdownMenuItem onClick={() => onEdit(item)}>
|
|
||||||
<PencilIcon className="mr-2 size-3.5" />
|
|
||||||
{t("knowledge.edit")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onDelete(item)}
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
>
|
>
|
||||||
<Trash2Icon className="mr-2 size-3.5" />
|
{expanded ? <ChevronDownIcon className="size-3.5" /> : <ChevronRightIcon className="size-3.5" />}
|
||||||
{t("knowledge.delete")}
|
</Button>
|
||||||
</DropdownMenuItem>
|
<FolderIcon className="size-4 shrink-0 text-muted-foreground" />
|
||||||
</DropdownMenuContent>
|
<button
|
||||||
</DropdownMenu>
|
type="button"
|
||||||
</div>
|
className="min-w-0 flex-1 truncate text-left"
|
||||||
|
onClick={() => onSelect(item.id)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-6 opacity-0 group-hover:opacity-100"
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
aria-label={t("knowledge.moreActions", { name: item.name })}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-3.5" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-40 min-w-40">
|
||||||
|
{item.parentId === 0 ? (
|
||||||
|
<DropdownMenuItem onClick={() => onCreate(item.id)}>
|
||||||
|
<PlusIcon className="mr-2 size-3.5" />
|
||||||
|
{t("knowledge.createSubDirectory")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
<DropdownMenuItem onClick={() => onEdit(item)}>
|
||||||
|
<PencilIcon className="mr-2 size-3.5" />
|
||||||
|
{t("knowledge.edit")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onDelete(item)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2Icon className="mr-2 size-3.5" />
|
||||||
|
{t("knowledge.delete")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-40">
|
||||||
|
{item.parentId === 0 ? (
|
||||||
|
<ContextMenuItem onClick={() => onCreate(item.id)}>
|
||||||
|
<PlusIcon className="mr-2 size-3.5" />
|
||||||
|
{t("knowledge.createSubDirectory")}
|
||||||
|
</ContextMenuItem>
|
||||||
|
) : null}
|
||||||
|
<ContextMenuItem onClick={() => onEdit(item)}>
|
||||||
|
<PencilIcon className="mr-2 size-3.5" />
|
||||||
|
{t("knowledge.edit")}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => onDelete(item)}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trash2Icon className="mr-2 size-3.5" />
|
||||||
|
{t("knowledge.delete")}
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
{expanded
|
{expanded
|
||||||
? (item.children || []).map((child) => (
|
? (item.children || []).map((child) => (
|
||||||
<DirectoryNode
|
<DirectoryNode
|
||||||
@@ -436,9 +475,11 @@ function DirectoryNode({
|
|||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
expandedIds={expandedIds}
|
expandedIds={expandedIds}
|
||||||
selectedDirectoryId={selectedDirectoryId}
|
selectedDirectoryId={selectedDirectoryId}
|
||||||
|
contextMenuDirectoryId={contextMenuDirectoryId}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
onContextMenuOpenChange={onContextMenuOpenChange}
|
||||||
onCreate={onCreate}
|
onCreate={onCreate}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
|||||||
Reference in New Issue
Block a user