feat: allow right click to create note and folder (#10523)

* feat: allow right click to create note and folder

* fix: duplicate menu for notes or folder

* fix: create notes in folder when a folder is selected
This commit is contained in:
ABucket 2025-10-10 23:58:14 +08:00 committed by kangfenmao
parent 80fc118465
commit 401e17eb0e
2 changed files with 200 additions and 126 deletions

View File

@ -385,21 +385,25 @@ const NotesPage: FC = () => {
}, [activeFilePath]) }, [activeFilePath])
// 获取目标文件夹路径(选中文件夹或根目录) // 获取目标文件夹路径(选中文件夹或根目录)
const getTargetFolderPath = useCallback(() => { const getTargetFolderPath = useCallback(
if (selectedFolderId) { (targetFolderId?: string) => {
const selectedNode = findNode(notesTree, selectedFolderId) const folderId = targetFolderId || selectedFolderId
if (selectedNode && selectedNode.type === 'folder') { if (folderId) {
return selectedNode.externalPath const selectedNode = findNode(notesTree, folderId)
if (selectedNode && selectedNode.type === 'folder') {
return selectedNode.externalPath
}
} }
} return notesPath // 默认返回根目录
return notesPath // 默认返回根目录 },
}, [selectedFolderId, notesTree, notesPath]) [selectedFolderId, notesTree, notesPath]
)
// 创建文件夹 // 创建文件夹
const handleCreateFolder = useCallback( const handleCreateFolder = useCallback(
async (name: string) => { async (name: string, targetFolderId?: string) => {
try { try {
const targetPath = getTargetFolderPath() const targetPath = getTargetFolderPath(targetFolderId)
if (!targetPath) { if (!targetPath) {
throw new Error('No folder path selected') throw new Error('No folder path selected')
} }
@ -415,11 +419,11 @@ const NotesPage: FC = () => {
// 创建笔记 // 创建笔记
const handleCreateNote = useCallback( const handleCreateNote = useCallback(
async (name: string) => { async (name: string, targetFolderId?: string) => {
try { try {
isCreatingNoteRef.current = true isCreatingNoteRef.current = true
const targetPath = getTargetFolderPath() const targetPath = getTargetFolderPath(targetFolderId)
if (!targetPath) { if (!targetPath) {
throw new Error('No folder path selected') throw new Error('No folder path selected')
} }

View File

@ -34,8 +34,8 @@ import { useSelector } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
interface NotesSidebarProps { interface NotesSidebarProps {
onCreateFolder: (name: string, parentId?: string) => void onCreateFolder: (name: string, targetFolderId?: string) => void
onCreateNote: (name: string, parentId?: string) => void onCreateNote: (name: string, targetFolderId?: string) => void
onSelectNode: (node: NotesTreeNode) => void onSelectNode: (node: NotesTreeNode) => void
onDeleteNode: (nodeId: string) => void onDeleteNode: (nodeId: string) => void
onRenameNode: (nodeId: string, newName: string) => void onRenameNode: (nodeId: string, newName: string) => void
@ -71,6 +71,8 @@ interface TreeNodeProps {
onDrop: (e: React.DragEvent, node: NotesTreeNode) => void onDrop: (e: React.DragEvent, node: NotesTreeNode) => void
onDragEnd: () => void onDragEnd: () => void
renderChildren?: boolean // 控制是否渲染子节点 renderChildren?: boolean // 控制是否渲染子节点
openDropdownKey: string | null
onDropdownOpenChange: (key: string | null) => void
} }
const TreeNode = memo<TreeNodeProps>( const TreeNode = memo<TreeNodeProps>(
@ -94,7 +96,9 @@ const TreeNode = memo<TreeNodeProps>(
onDragLeave, onDragLeave,
onDrop, onDrop,
onDragEnd, onDragEnd,
renderChildren = true renderChildren = true,
openDropdownKey,
onDropdownOpenChange
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -119,8 +123,12 @@ const TreeNode = memo<TreeNodeProps>(
return ( return (
<div key={node.id}> <div key={node.id}>
<Dropdown menu={{ items: getMenuItems(node) }} trigger={['contextMenu']}> <Dropdown
<div> menu={{ items: getMenuItems(node) }}
trigger={['contextMenu']}
open={openDropdownKey === node.id}
onOpenChange={(open) => onDropdownOpenChange(open ? node.id : null)}>
<div onContextMenu={(e) => e.stopPropagation()}>
<TreeNodeContainer <TreeNodeContainer
active={isActive} active={isActive}
depth={depth} depth={depth}
@ -206,6 +214,8 @@ const TreeNode = memo<TreeNodeProps>(
onDrop={onDrop} onDrop={onDrop}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
renderChildren={renderChildren} renderChildren={renderChildren}
openDropdownKey={openDropdownKey}
onDropdownOpenChange={onDropdownOpenChange}
/> />
))} ))}
</div> </div>
@ -244,6 +254,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const [isShowSearch, setIsShowSearch] = useState(false) const [isShowSearch, setIsShowSearch] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('') const [searchKeyword, setSearchKeyword] = useState('')
const [isDragOverSidebar, setIsDragOverSidebar] = useState(false) const [isDragOverSidebar, setIsDragOverSidebar] = useState(false)
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null)
const dragNodeRef = useRef<HTMLDivElement | null>(null) const dragNodeRef = useRef<HTMLDivElement | null>(null)
const scrollbarRef = useRef<any>(null) const scrollbarRef = useRef<any>(null)
@ -571,6 +582,28 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
}) })
} }
if (node.type === 'folder') {
baseMenuItems.push(
{
label: t('notes.new_note'),
key: 'new_note',
icon: <FilePlus size={14} />,
onClick: () => {
onCreateNote(t('notes.untitled_note'), node.id)
}
},
{
label: t('notes.new_folder'),
key: 'new_folder',
icon: <Folder size={14} />,
onClick: () => {
onCreateFolder(t('notes.untitled_folder'), node.id)
}
},
{ type: 'divider' }
)
}
baseMenuItems.push( baseMenuItems.push(
{ {
label: t('notes.rename'), label: t('notes.rename'),
@ -674,7 +707,9 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
handleDeleteNode, handleDeleteNode,
renamingNodeIds, renamingNodeIds,
handleAutoRename, handleAutoRename,
exportMenuOptions exportMenuOptions,
onCreateNote,
onCreateFolder
] ]
) )
@ -755,6 +790,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
fileInput.click() fileInput.click()
}, [onUploadFiles]) }, [onUploadFiles])
const getEmptyAreaMenuItems = useCallback((): MenuProps['items'] => {
return [
{
label: t('notes.new_note'),
key: 'new_note',
icon: <FilePlus size={14} />,
onClick: handleCreateNote
},
{
label: t('notes.new_folder'),
key: 'new_folder',
icon: <Folder size={14} />,
onClick: handleCreateFolder
}
]
}, [t, handleCreateNote, handleCreateFolder])
return ( return (
<SidebarContainer <SidebarContainer
onDragOver={(e) => { onDragOver={(e) => {
@ -784,31 +836,90 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
<NotesTreeContainer> <NotesTreeContainer>
{shouldUseVirtualization ? ( {shouldUseVirtualization ? (
<VirtualizedTreeContainer ref={parentRef}> <Dropdown
<div menu={{ items: getEmptyAreaMenuItems() }}
style={{ trigger={['contextMenu']}
height: `${virtualizer.getTotalSize()}px`, open={openDropdownKey === 'empty-area'}
width: '100%', onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}>
position: 'relative' <VirtualizedTreeContainer ref={parentRef}>
}}> <div
{virtualizer.getVirtualItems().map((virtualItem) => { style={{
const { node, depth } = flattenedNodes[virtualItem.index] height: `${virtualizer.getTotalSize()}px`,
return ( width: '100%',
<div position: 'relative'
key={virtualItem.key} }}>
data-index={virtualItem.index} {virtualizer.getVirtualItems().map((virtualItem) => {
ref={virtualizer.measureElement} const { node, depth } = flattenedNodes[virtualItem.index]
style={{ return (
position: 'absolute', <div
top: 0, key={virtualItem.key}
left: 0, data-index={virtualItem.index}
width: '100%', ref={virtualizer.measureElement}
transform: `translateY(${virtualItem.start}px)` style={{
}}> position: 'absolute',
<div style={{ padding: '0 8px' }}> top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`
}}>
<div style={{ padding: '0 8px' }}>
<TreeNode
node={node}
depth={depth}
selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id}
editingNodeId={editingNodeId}
renamingNodeIds={renamingNodeIds}
newlyRenamedNodeIds={newlyRenamedNodeIds}
draggedNodeId={draggedNodeId}
dragOverNodeId={dragOverNodeId}
dragPosition={dragPosition}
inPlaceEdit={inPlaceEdit}
getMenuItems={getMenuItems}
onSelectNode={onSelectNode}
onToggleExpanded={onToggleExpanded}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
renderChildren={false}
openDropdownKey={openDropdownKey}
onDropdownOpenChange={setOpenDropdownKey}
/>
</div>
</div>
)
})}
</div>
{!isShowStarred && !isShowSearch && (
<DropHintNode>
<TreeNodeContainer active={false} depth={0}>
<TreeNodeContent>
<NodeIcon>
<FilePlus size={16} />
</NodeIcon>
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
</TreeNodeContent>
</TreeNodeContainer>
</DropHintNode>
)}
</VirtualizedTreeContainer>
</Dropdown>
) : (
<Dropdown
menu={{ items: getEmptyAreaMenuItems() }}
trigger={['contextMenu']}
open={openDropdownKey === 'empty-area'}
onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}>
<StyledScrollbar ref={scrollbarRef}>
<TreeContent>
{isShowStarred || isShowSearch
? filteredTree.map((node) => (
<TreeNode <TreeNode
key={node.id}
node={node} node={node}
depth={depth} depth={0}
selectedFolderId={selectedFolderId} selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id} activeNodeId={activeNode?.id}
editingNodeId={editingNodeId} editingNodeId={editingNodeId}
@ -826,92 +937,51 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
renderChildren={false} openDropdownKey={openDropdownKey}
onDropdownOpenChange={setOpenDropdownKey}
/> />
</div> ))
</div> : notesTree.map((node) => (
) <TreeNode
})} key={node.id}
</div> node={node}
{!isShowStarred && !isShowSearch && ( depth={0}
<DropHintNode> selectedFolderId={selectedFolderId}
<TreeNodeContainer active={false} depth={0}> activeNodeId={activeNode?.id}
<TreeNodeContent> editingNodeId={editingNodeId}
<NodeIcon> renamingNodeIds={renamingNodeIds}
<FilePlus size={16} /> newlyRenamedNodeIds={newlyRenamedNodeIds}
</NodeIcon> draggedNodeId={draggedNodeId}
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText> dragOverNodeId={dragOverNodeId}
</TreeNodeContent> dragPosition={dragPosition}
</TreeNodeContainer> inPlaceEdit={inPlaceEdit}
</DropHintNode> getMenuItems={getMenuItems}
)} onSelectNode={onSelectNode}
</VirtualizedTreeContainer> onToggleExpanded={onToggleExpanded}
) : ( onDragStart={handleDragStart}
<StyledScrollbar ref={scrollbarRef}> onDragOver={handleDragOver}
<TreeContent> onDragLeave={handleDragLeave}
{isShowStarred || isShowSearch onDrop={handleDrop}
? filteredTree.map((node) => ( onDragEnd={handleDragEnd}
<TreeNode openDropdownKey={openDropdownKey}
key={node.id} onDropdownOpenChange={setOpenDropdownKey}
node={node} />
depth={0} ))}
selectedFolderId={selectedFolderId} {!isShowStarred && !isShowSearch && (
activeNodeId={activeNode?.id} <DropHintNode>
editingNodeId={editingNodeId} <TreeNodeContainer active={false} depth={0}>
renamingNodeIds={renamingNodeIds} <TreeNodeContent>
newlyRenamedNodeIds={newlyRenamedNodeIds} <NodeIcon>
draggedNodeId={draggedNodeId} <FilePlus size={16} />
dragOverNodeId={dragOverNodeId} </NodeIcon>
dragPosition={dragPosition} <DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
inPlaceEdit={inPlaceEdit} </TreeNodeContent>
getMenuItems={getMenuItems} </TreeNodeContainer>
onSelectNode={onSelectNode} </DropHintNode>
onToggleExpanded={onToggleExpanded} )}
onDragStart={handleDragStart} </TreeContent>
onDragOver={handleDragOver} </StyledScrollbar>
onDragLeave={handleDragLeave} </Dropdown>
onDrop={handleDrop}
onDragEnd={handleDragEnd}
/>
))
: notesTree.map((node) => (
<TreeNode
key={node.id}
node={node}
depth={0}
selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id}
editingNodeId={editingNodeId}
renamingNodeIds={renamingNodeIds}
newlyRenamedNodeIds={newlyRenamedNodeIds}
draggedNodeId={draggedNodeId}
dragOverNodeId={dragOverNodeId}
dragPosition={dragPosition}
inPlaceEdit={inPlaceEdit}
getMenuItems={getMenuItems}
onSelectNode={onSelectNode}
onToggleExpanded={onToggleExpanded}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
/>
))}
{!isShowStarred && !isShowSearch && (
<DropHintNode>
<TreeNodeContainer active={false} depth={0}>
<TreeNodeContent>
<NodeIcon>
<FilePlus size={16} />
</NodeIcon>
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
</TreeNodeContent>
</TreeNodeContainer>
</DropHintNode>
)}
</TreeContent>
</StyledScrollbar>
)} )}
</NotesTreeContainer> </NotesTreeContainer>