mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 11:44:28 +08:00
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:
parent
80fc118465
commit
401e17eb0e
@ -385,21 +385,25 @@ const NotesPage: FC = () => {
|
||||
}, [activeFilePath])
|
||||
|
||||
// 获取目标文件夹路径(选中文件夹或根目录)
|
||||
const getTargetFolderPath = useCallback(() => {
|
||||
if (selectedFolderId) {
|
||||
const selectedNode = findNode(notesTree, selectedFolderId)
|
||||
if (selectedNode && selectedNode.type === 'folder') {
|
||||
return selectedNode.externalPath
|
||||
const getTargetFolderPath = useCallback(
|
||||
(targetFolderId?: string) => {
|
||||
const folderId = targetFolderId || selectedFolderId
|
||||
if (folderId) {
|
||||
const selectedNode = findNode(notesTree, folderId)
|
||||
if (selectedNode && selectedNode.type === 'folder') {
|
||||
return selectedNode.externalPath
|
||||
}
|
||||
}
|
||||
}
|
||||
return notesPath // 默认返回根目录
|
||||
}, [selectedFolderId, notesTree, notesPath])
|
||||
return notesPath // 默认返回根目录
|
||||
},
|
||||
[selectedFolderId, notesTree, notesPath]
|
||||
)
|
||||
|
||||
// 创建文件夹
|
||||
const handleCreateFolder = useCallback(
|
||||
async (name: string) => {
|
||||
async (name: string, targetFolderId?: string) => {
|
||||
try {
|
||||
const targetPath = getTargetFolderPath()
|
||||
const targetPath = getTargetFolderPath(targetFolderId)
|
||||
if (!targetPath) {
|
||||
throw new Error('No folder path selected')
|
||||
}
|
||||
@ -415,11 +419,11 @@ const NotesPage: FC = () => {
|
||||
|
||||
// 创建笔记
|
||||
const handleCreateNote = useCallback(
|
||||
async (name: string) => {
|
||||
async (name: string, targetFolderId?: string) => {
|
||||
try {
|
||||
isCreatingNoteRef.current = true
|
||||
|
||||
const targetPath = getTargetFolderPath()
|
||||
const targetPath = getTargetFolderPath(targetFolderId)
|
||||
if (!targetPath) {
|
||||
throw new Error('No folder path selected')
|
||||
}
|
||||
|
||||
@ -34,8 +34,8 @@ import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface NotesSidebarProps {
|
||||
onCreateFolder: (name: string, parentId?: string) => void
|
||||
onCreateNote: (name: string, parentId?: string) => void
|
||||
onCreateFolder: (name: string, targetFolderId?: string) => void
|
||||
onCreateNote: (name: string, targetFolderId?: string) => void
|
||||
onSelectNode: (node: NotesTreeNode) => void
|
||||
onDeleteNode: (nodeId: string) => void
|
||||
onRenameNode: (nodeId: string, newName: string) => void
|
||||
@ -71,6 +71,8 @@ interface TreeNodeProps {
|
||||
onDrop: (e: React.DragEvent, node: NotesTreeNode) => void
|
||||
onDragEnd: () => void
|
||||
renderChildren?: boolean // 控制是否渲染子节点
|
||||
openDropdownKey: string | null
|
||||
onDropdownOpenChange: (key: string | null) => void
|
||||
}
|
||||
|
||||
const TreeNode = memo<TreeNodeProps>(
|
||||
@ -94,7 +96,9 @@ const TreeNode = memo<TreeNodeProps>(
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
renderChildren = true
|
||||
renderChildren = true,
|
||||
openDropdownKey,
|
||||
onDropdownOpenChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -119,8 +123,12 @@ const TreeNode = memo<TreeNodeProps>(
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<Dropdown menu={{ items: getMenuItems(node) }} trigger={['contextMenu']}>
|
||||
<div>
|
||||
<Dropdown
|
||||
menu={{ items: getMenuItems(node) }}
|
||||
trigger={['contextMenu']}
|
||||
open={openDropdownKey === node.id}
|
||||
onOpenChange={(open) => onDropdownOpenChange(open ? node.id : null)}>
|
||||
<div onContextMenu={(e) => e.stopPropagation()}>
|
||||
<TreeNodeContainer
|
||||
active={isActive}
|
||||
depth={depth}
|
||||
@ -206,6 +214,8 @@ const TreeNode = memo<TreeNodeProps>(
|
||||
onDrop={onDrop}
|
||||
onDragEnd={onDragEnd}
|
||||
renderChildren={renderChildren}
|
||||
openDropdownKey={openDropdownKey}
|
||||
onDropdownOpenChange={onDropdownOpenChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -244,6 +254,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
const [isShowSearch, setIsShowSearch] = useState(false)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [isDragOverSidebar, setIsDragOverSidebar] = useState(false)
|
||||
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null)
|
||||
const dragNodeRef = useRef<HTMLDivElement | null>(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(
|
||||
{
|
||||
label: t('notes.rename'),
|
||||
@ -674,7 +707,9 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
handleDeleteNode,
|
||||
renamingNodeIds,
|
||||
handleAutoRename,
|
||||
exportMenuOptions
|
||||
exportMenuOptions,
|
||||
onCreateNote,
|
||||
onCreateFolder
|
||||
]
|
||||
)
|
||||
|
||||
@ -755,6 +790,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
fileInput.click()
|
||||
}, [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 (
|
||||
<SidebarContainer
|
||||
onDragOver={(e) => {
|
||||
@ -784,31 +836,90 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
|
||||
<NotesTreeContainer>
|
||||
{shouldUseVirtualization ? (
|
||||
<VirtualizedTreeContainer ref={parentRef}>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const { node, depth } = flattenedNodes[virtualItem.index]
|
||||
return (
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItem.start}px)`
|
||||
}}>
|
||||
<div style={{ padding: '0 8px' }}>
|
||||
<Dropdown
|
||||
menu={{ items: getEmptyAreaMenuItems() }}
|
||||
trigger={['contextMenu']}
|
||||
open={openDropdownKey === 'empty-area'}
|
||||
onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}>
|
||||
<VirtualizedTreeContainer ref={parentRef}>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const { node, depth } = flattenedNodes[virtualItem.index]
|
||||
return (
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
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
|
||||
key={node.id}
|
||||
node={node}
|
||||
depth={depth}
|
||||
depth={0}
|
||||
selectedFolderId={selectedFolderId}
|
||||
activeNodeId={activeNode?.id}
|
||||
editingNodeId={editingNodeId}
|
||||
@ -826,92 +937,51 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
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>
|
||||
) : (
|
||||
<StyledScrollbar ref={scrollbarRef}>
|
||||
<TreeContent>
|
||||
{isShowStarred || isShowSearch
|
||||
? filteredTree.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}
|
||||
/>
|
||||
))
|
||||
: 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>
|
||||
))
|
||||
: 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}
|
||||
openDropdownKey={openDropdownKey}
|
||||
onDropdownOpenChange={setOpenDropdownKey}
|
||||
/>
|
||||
))}
|
||||
{!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>
|
||||
</Dropdown>
|
||||
)}
|
||||
</NotesTreeContainer>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user