fix: update file upload handling and remove unused root selection

This commit is contained in:
suyao 2025-12-08 00:32:43 +08:00
parent 2863961349
commit bcf1fa90ed
No known key found for this signature in database
3 changed files with 75 additions and 51 deletions

View File

@ -574,12 +574,6 @@ const NotesPage: FC = () => {
[dispatch, handleToggleExpanded, invalidateFileContent] [dispatch, handleToggleExpanded, invalidateFileContent]
) )
// 选中根(清空选择)
const handleSelectRoot = useCallback(() => {
dispatch(setActiveFilePath(undefined))
setSelectedFolderId(null)
}, [dispatch])
// 删除节点 // 删除节点
const handleDeleteNode = useCallback( const handleDeleteNode = useCallback(
async (nodeId: string) => { async (nodeId: string) => {
@ -658,14 +652,19 @@ const NotesPage: FC = () => {
// 处理文件上传 // 处理文件上传
const handleUploadFiles = useCallback( const handleUploadFiles = useCallback(
async (files: File[]) => { async (
files:
| File[]
| Array<{ fullPath: string; isFile: boolean; isDirectory: boolean; systemPath: string }>,
overrideTargetFolderPath?: string
) => {
try { try {
if (!files || files.length === 0) { if (!files || files.length === 0) {
window.toast.warning(t('notes.no_file_selected')) window.toast.warning(t('notes.no_file_selected'))
return return
} }
const targetFolderPath = getTargetFolderPath() const targetFolderPath = overrideTargetFolderPath || getTargetFolderPath()
if (!targetFolderPath) { if (!targetFolderPath) {
throw new Error('No folder path selected') throw new Error('No folder path selected')
} }
@ -937,7 +936,6 @@ const NotesPage: FC = () => {
notesTree={notesTree} notesTree={notesTree}
selectedFolderId={selectedFolderId} selectedFolderId={selectedFolderId}
onSelectNode={handleSelectNode} onSelectNode={handleSelectNode}
onSelectRoot={handleSelectRoot}
onCreateFolder={handleCreateFolder} onCreateFolder={handleCreateFolder}
onCreateNote={handleCreateNote} onCreateNote={handleCreateNote}
onDeleteNode={handleDeleteNode} onDeleteNode={handleDeleteNode}

View File

@ -1,7 +1,7 @@
import { DynamicVirtualList } from '@renderer/components/VirtualList' import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { useActiveNode } from '@renderer/hooks/useNotesQuery' import { useActiveNode } from '@renderer/hooks/useNotesQuery'
import NotesSidebarHeader from '@renderer/pages/notes/NotesSidebarHeader' import NotesSidebarHeader from '@renderer/pages/notes/NotesSidebarHeader'
import { findNode } from '@renderer/services/NotesService' import { findNode, findParent } from '@renderer/services/NotesTreeService'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { selectSortType } from '@renderer/store/note' import { selectSortType } from '@renderer/store/note'
import type { NotesSortType, NotesTreeNode } from '@renderer/types/note' import type { NotesSortType, NotesTreeNode } from '@renderer/types/note'
@ -32,14 +32,16 @@ interface NotesSidebarProps {
onCreateFolder: (name: string, targetFolderId?: string) => void onCreateFolder: (name: string, targetFolderId?: string) => void
onCreateNote: (name: string, targetFolderId?: string) => void onCreateNote: (name: string, targetFolderId?: string) => void
onSelectNode: (node: NotesTreeNode) => void onSelectNode: (node: NotesTreeNode) => void
onSelectRoot: () => void
onDeleteNode: (nodeId: string) => void onDeleteNode: (nodeId: string) => void
onRenameNode: (nodeId: string, newName: string) => void onRenameNode: (nodeId: string, newName: string) => void
onToggleExpanded: (nodeId: string) => void onToggleExpanded: (nodeId: string) => void
onToggleStar: (nodeId: string) => void onToggleStar: (nodeId: string) => void
onMoveNode: (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => void onMoveNode: (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => void
onSortNodes: (sortType: NotesSortType) => void onSortNodes: (sortType: NotesSortType) => void
onUploadFiles: (files: File[]) => void onUploadFiles: (
files: File[] | Array<{ fullPath: string; isFile: boolean; isDirectory: boolean; systemPath: string }>,
targetFolderPath?: string
) => void
notesTree: NotesTreeNode[] notesTree: NotesTreeNode[]
selectedFolderId?: string | null selectedFolderId?: string | null
notesPath?: string notesPath?: string
@ -50,7 +52,6 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
onCreateFolder, onCreateFolder,
onCreateNote, onCreateNote,
onSelectNode, onSelectNode,
onSelectRoot,
onDeleteNode, onDeleteNode,
onRenameNode, onRenameNode,
onToggleExpanded, onToggleExpanded,
@ -97,6 +98,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
onUploadFiles, onUploadFiles,
setIsDragOverSidebar, setIsDragOverSidebar,
getTargetFolderPath: () => { getTargetFolderPath: () => {
// Default target folder path (used by select dialogs and as fallback)
if (selectedFolderId) { if (selectedFolderId) {
const selectedNode = findNode(notesTree, selectedFolderId) const selectedNode = findNode(notesTree, selectedFolderId)
if (selectedNode && selectedNode.type === 'folder') { if (selectedNode && selectedNode.type === 'folder') {
@ -394,7 +396,24 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
onDragLeave={() => setIsDragOverSidebar(false)} onDragLeave={() => setIsDragOverSidebar(false)}
onDrop={(e) => { onDrop={(e) => {
if (!draggedNodeId) { if (!draggedNodeId) {
handleDropFiles(e) // External drop: determine precise target folder by hit element
let overrideTarget: string | undefined
const targetEl = (e.target as HTMLElement).closest('[data-node-id]') as HTMLElement | null
if (targetEl) {
const nodeId = targetEl.getAttribute('data-node-id') || ''
const node = nodeId ? findNode(notesTree, nodeId) : null
if (node) {
if (node.type === 'folder') {
overrideTarget = node.externalPath
} else {
const parent = findParent(notesTree, node.id)
if (parent && parent.type === 'folder') {
overrideTarget = parent.externalPath
}
}
}
}
handleDropFiles(e, overrideTarget)
} }
}}> }}>
<NotesSidebarHeader <NotesSidebarHeader
@ -436,40 +455,44 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
trigger={['contextMenu']} trigger={['contextMenu']}
open={openDropdownKey === 'empty-area'} open={openDropdownKey === 'empty-area'}
onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}> onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}>
<DynamicVirtualList <div
ref={virtualListRef} style={{ flex: 1, minHeight: 0 }}
list={flattenedNodes} onContextMenu={(e) => {
size={0} const target = e.target as HTMLElement
estimateSize={() => 28} // Right-click on empty space should not change selection
scrollerStyle={{ flex: 1, minHeight: 0, height: 'auto' }} if (!target.closest('[data-index]')) {
itemContainerStyle={{ padding: '8px 8px 0 8px' }} setOpenDropdownKey('empty-area')
overscan={10} e.preventDefault()
isSticky={isSticky}
getItemDepth={getItemDepth}
containerProps={{
onContextMenu: (e) => {
const target = e.target as HTMLElement
if (!target.closest('[data-index]')) {
onSelectRoot()
setOpenDropdownKey('empty-area')
}
},
onClick: (e) => {
const target = e.target as HTMLElement
if (!target.closest('[data-index]')) {
onSelectRoot()
}
} }
}}> }}>
{({ node, depth }) => ( <DynamicVirtualList
<TreeNode ref={virtualListRef}
node={node} list={flattenedNodes}
depth={depth} estimateSize={() => 28}
renderChildren={false} scrollerStyle={{ flex: 1, minHeight: 0, height: '100%' }}
onHintClick={node.type === 'hint' ? handleSelectFolder : undefined} itemContainerStyle={{ padding: '8px 8px 0 8px' }}
/> overscan={10}
)} isSticky={isSticky}
</DynamicVirtualList> getItemDepth={getItemDepth}
containerProps={{
// Double click on empty area: create a new note (.md)
onDoubleClick: (e) => {
const target = e.target as HTMLElement
if (!target.closest('[data-index]')) {
handleCreateNote()
}
}
}}>
{({ node, depth }) => (
<TreeNode
node={node}
depth={depth}
renderChildren={false}
onHintClick={node.type === 'hint' ? handleSelectFolder : undefined}
/>
)}
</DynamicVirtualList>
</div>
</Dropdown> </Dropdown>
</NotesTreeContainer> </NotesTreeContainer>

View File

@ -5,7 +5,10 @@ import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('useNotesFileUpload') const logger = loggerService.withContext('useNotesFileUpload')
interface UseNotesFileUploadProps { interface UseNotesFileUploadProps {
onUploadFiles: (files: File[]) => void onUploadFiles: (
files: File[] | Array<{ fullPath: string; isFile: boolean; isDirectory: boolean; systemPath: string }>,
targetFolderPath?: string
) => void
setIsDragOverSidebar: (isDragOver: boolean) => void setIsDragOverSidebar: (isDragOver: boolean) => void
getTargetFolderPath?: () => string | null getTargetFolderPath?: () => string | null
refreshTree?: () => Promise<void> refreshTree?: () => Promise<void>
@ -25,7 +28,7 @@ export const useNotesFileUpload = ({
* This ensures dragging ~/Users/me/tmp/xxx creates target/tmp/xxx * This ensures dragging ~/Users/me/tmp/xxx creates target/tmp/xxx
*/ */
const handleDropFiles = useCallback( const handleDropFiles = useCallback(
async (e: React.DragEvent) => { async (e: React.DragEvent, overrideTargetFolderPath?: string) => {
e.preventDefault() e.preventDefault()
setIsDragOverSidebar(false) setIsDragOverSidebar(false)
@ -113,14 +116,14 @@ export const useNotesFileUpload = ({
await Promise.all(promises) await Promise.all(promises)
if (entryDataList.length > 0) { if (entryDataList.length > 0) {
// Pass entry data list to parent for recursive upload // Pass entry data list to parent for recursive upload with optional target override
onUploadFiles(entryDataList as any) onUploadFiles(entryDataList as any, overrideTargetFolderPath)
} }
} else { } else {
// Fallback for browsers without FileSystemEntry API // Fallback for browsers without FileSystemEntry API
const regularFiles = Array.from(e.dataTransfer.files) const regularFiles = Array.from(e.dataTransfer.files)
if (regularFiles.length > 0) { if (regularFiles.length > 0) {
onUploadFiles(regularFiles) onUploadFiles(regularFiles, overrideTargetFolderPath)
} }
} }
}, },