mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
feat: enhance context menu functionality in DocumentNode and FolderNode components
This commit is contained in:
parent
ddccba0df8
commit
13f4b175a6
3 changed files with 45 additions and 35 deletions
|
|
@ -10,14 +10,12 @@ import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||||
|
|
@ -41,6 +39,8 @@ interface DocumentNodeProps {
|
||||||
onEdit: (doc: DocumentNodeDoc) => void;
|
onEdit: (doc: DocumentNodeDoc) => void;
|
||||||
onDelete: (doc: DocumentNodeDoc) => void;
|
onDelete: (doc: DocumentNodeDoc) => void;
|
||||||
onMove: (doc: DocumentNodeDoc) => void;
|
onMove: (doc: DocumentNodeDoc) => void;
|
||||||
|
contextMenuOpen?: boolean;
|
||||||
|
onContextMenuOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentNode = React.memo(function DocumentNode({
|
export const DocumentNode = React.memo(function DocumentNode({
|
||||||
|
|
@ -52,6 +52,8 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onMove,
|
onMove,
|
||||||
|
contextMenuOpen,
|
||||||
|
onContextMenuOpenChange,
|
||||||
}: DocumentNodeProps) {
|
}: DocumentNodeProps) {
|
||||||
const statusState = doc.status?.state ?? "ready";
|
const statusState = doc.status?.state ?? "ready";
|
||||||
const isSelectable = statusState !== "pending" && statusState !== "processing";
|
const isSelectable = statusState !== "pending" && statusState !== "processing";
|
||||||
|
|
@ -76,7 +78,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
const isProcessing = statusState === "pending" || statusState === "processing";
|
const isProcessing = statusState === "pending" || statusState === "processing";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu onOpenChange={onContextMenuOpenChange}>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag ref */}
|
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag ref */}
|
||||||
<div
|
<div
|
||||||
|
|
@ -131,7 +133,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
className="hidden sm:inline-flex h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
|
|
@ -152,7 +154,6 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
<Move className="mr-2 h-4 w-4" />
|
<Move className="mr-2 h-4 w-4" />
|
||||||
Move to...
|
Move to...
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
|
|
@ -166,6 +167,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
|
|
||||||
|
{contextMenuOpen && (
|
||||||
<ContextMenuContent className="w-40">
|
<ContextMenuContent className="w-40">
|
||||||
<ContextMenuItem onClick={() => onPreview(doc)}>
|
<ContextMenuItem onClick={() => onPreview(doc)}>
|
||||||
<Eye className="mr-2 h-4 w-4" />
|
<Eye className="mr-2 h-4 w-4" />
|
||||||
|
|
@ -181,7 +183,6 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
<Move className="mr-2 h-4 w-4" />
|
<Move className="mr-2 h-4 w-4" />
|
||||||
Move to...
|
Move to...
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
|
|
@ -191,6 +192,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
Delete
|
Delete
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ interface FolderNodeProps {
|
||||||
onReorderFolder?: (folderId: number, beforePos: string | null, afterPos: string | null) => void;
|
onReorderFolder?: (folderId: number, beforePos: string | null, afterPos: string | null) => void;
|
||||||
siblingPositions?: { before: string | null; after: string | null };
|
siblingPositions?: { before: string | null; after: string | null };
|
||||||
disabledDropIds?: Set<number>;
|
disabledDropIds?: Set<number>;
|
||||||
|
contextMenuOpen?: boolean;
|
||||||
|
onContextMenuOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDropZone(
|
function getDropZone(
|
||||||
|
|
@ -99,6 +101,8 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
onReorderFolder,
|
onReorderFolder,
|
||||||
siblingPositions,
|
siblingPositions,
|
||||||
disabledDropIds,
|
disabledDropIds,
|
||||||
|
contextMenuOpen,
|
||||||
|
onContextMenuOpenChange,
|
||||||
}: FolderNodeProps) {
|
}: FolderNodeProps) {
|
||||||
const [renameValue, setRenameValue] = useState(folder.name);
|
const [renameValue, setRenameValue] = useState(folder.name);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -213,7 +217,7 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
const FolderIcon = isExpanded ? FolderOpen : Folder;
|
const FolderIcon = isExpanded ? FolderOpen : Folder;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu onOpenChange={onContextMenuOpenChange}>
|
||||||
<ContextMenuTrigger asChild disabled={isRenaming}>
|
<ContextMenuTrigger asChild disabled={isRenaming}>
|
||||||
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag/drop refs */}
|
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag/drop refs */}
|
||||||
<div
|
<div
|
||||||
|
|
@ -279,7 +283,7 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
className="hidden sm:inline-flex h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
|
|
@ -313,7 +317,6 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
<Move className="mr-2 h-4 w-4" />
|
<Move className="mr-2 h-4 w-4" />
|
||||||
Move to...
|
Move to...
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
@ -330,7 +333,7 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
|
|
||||||
{!isRenaming && (
|
{!isRenaming && contextMenuOpen && (
|
||||||
<ContextMenuContent className="w-40">
|
<ContextMenuContent className="w-40">
|
||||||
<ContextMenuItem onClick={() => onCreateSubfolder(folder.id)}>
|
<ContextMenuItem onClick={() => onCreateSubfolder(folder.id)}>
|
||||||
<FolderPlus className="mr-2 h-4 w-4" />
|
<FolderPlus className="mr-2 h-4 w-4" />
|
||||||
|
|
@ -344,7 +347,6 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
<Move className="mr-2 h-4 w-4" />
|
<Move className="mr-2 h-4 w-4" />
|
||||||
Move to...
|
Move to...
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
onClick={() => onDelete(folder)}
|
onClick={() => onDelete(folder)}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { TreePine } from "lucide-react";
|
import { TreePine } from "lucide-react";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
import { renamingFolderIdAtom } from "@/atoms/documents/folder.atoms";
|
import { renamingFolderIdAtom } from "@/atoms/documents/folder.atoms";
|
||||||
|
|
@ -80,6 +80,8 @@ export function FolderTreeView({
|
||||||
return counts;
|
return counts;
|
||||||
}, [folders, foldersByParent, docsByFolder]);
|
}, [folders, foldersByParent, docsByFolder]);
|
||||||
|
|
||||||
|
const [openContextMenuId, setOpenContextMenuId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Single subscription for rename state — derived boolean passed to each FolderNode
|
// Single subscription for rename state — derived boolean passed to each FolderNode
|
||||||
const [renamingFolderId, setRenamingFolderId] = useAtom(renamingFolderIdAtom);
|
const [renamingFolderId, setRenamingFolderId] = useAtom(renamingFolderIdAtom);
|
||||||
const handleStartRename = useCallback(
|
const handleStartRename = useCallback(
|
||||||
|
|
@ -157,6 +159,8 @@ export function FolderTreeView({
|
||||||
onDropIntoFolder={onDropIntoFolder}
|
onDropIntoFolder={onDropIntoFolder}
|
||||||
onReorderFolder={onReorderFolder}
|
onReorderFolder={onReorderFolder}
|
||||||
siblingPositions={siblingPositions}
|
siblingPositions={siblingPositions}
|
||||||
|
contextMenuOpen={openContextMenuId === `folder-${f.id}`}
|
||||||
|
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `folder-${f.id}` : null)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -177,6 +181,8 @@ export function FolderTreeView({
|
||||||
onEdit={onEditDocument}
|
onEdit={onEditDocument}
|
||||||
onDelete={onDeleteDocument}
|
onDelete={onDeleteDocument}
|
||||||
onMove={onMoveDocument}
|
onMove={onMoveDocument}
|
||||||
|
contextMenuOpen={openContextMenuId === `doc-${d.id}`}
|
||||||
|
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `doc-${d.id}` : null)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue