mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 05:39:05 +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])
|
}, [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')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user