refactor: use TypeScript function overloading for uploadNotes type safety (#11833)

* Initial plan

* refactor: use TypeScript function overloading for uploadNotes

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>
This commit is contained in:
Copilot 2025-12-11 12:59:50 +08:00 committed by GitHub
parent 377614713b
commit 82628edbd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 46 additions and 40 deletions

View File

@ -13,7 +13,8 @@ import {
loadTree, loadTree,
renameNode as renameEntry, renameNode as renameEntry,
sortTree, sortTree,
uploadNotes uploadNotes,
type FileEntryData
} from '@renderer/services/NotesService' } from '@renderer/services/NotesService'
import { import {
addUniquePath, addUniquePath,
@ -652,10 +653,7 @@ const NotesPage: FC = () => {
// 处理文件上传 // 处理文件上传
const handleUploadFiles = useCallback( const handleUploadFiles = useCallback(
async ( async (files: File[] | FileEntryData[], overrideTargetFolderPath?: string) => {
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'))

View File

@ -2,6 +2,7 @@ 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, findParent } from '@renderer/services/NotesTreeService' import { findNode, findParent } from '@renderer/services/NotesTreeService'
import { type FileEntryData } from '@renderer/services/NotesService'
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'
@ -38,10 +39,7 @@ interface NotesSidebarProps {
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: ( onUploadFiles: (files: File[] | FileEntryData[], targetFolderPath?: string) => void
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

View File

@ -1,14 +1,12 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { type FileEntryData } from '@renderer/services/NotesService'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('useNotesFileUpload') const logger = loggerService.withContext('useNotesFileUpload')
interface UseNotesFileUploadProps { interface UseNotesFileUploadProps {
onUploadFiles: ( onUploadFiles: (files: File[] | FileEntryData[], targetFolderPath?: string) => void
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>
@ -36,12 +34,7 @@ export const useNotesFileUpload = ({
if (items.length === 0) return if (items.length === 0) return
// Collect all entries with their fullPath preserved // Collect all entries with their fullPath preserved
const entryDataList: Array<{ const entryDataList: FileEntryData[] = []
fullPath: string
isFile: boolean
isDirectory: boolean
systemPath: string
}> = []
const processEntry = async (entry: FileSystemEntry): Promise<void> => { const processEntry = async (entry: FileSystemEntry): Promise<void> => {
if (entry.isFile) { if (entry.isFile) {
@ -117,7 +110,7 @@ export const useNotesFileUpload = ({
if (entryDataList.length > 0) { if (entryDataList.length > 0) {
// Pass entry data list to parent for recursive upload with optional target override // Pass entry data list to parent for recursive upload with optional target override
onUploadFiles(entryDataList as any, overrideTargetFolderPath) onUploadFiles(entryDataList, overrideTargetFolderPath)
} }
} else { } else {
// Fallback for browsers without FileSystemEntry API // Fallback for browsers without FileSystemEntry API

View File

@ -15,6 +15,13 @@ export interface UploadResult {
folderCount: number folderCount: number
} }
export interface FileEntryData {
fullPath: string
isFile: boolean
isDirectory: boolean
systemPath: string
}
export async function loadTree(rootPath: string): Promise<NotesTreeNode[]> { export async function loadTree(rootPath: string): Promise<NotesTreeNode[]> {
return window.api.file.getDirectoryStructure(normalizePath(rootPath)) return window.api.file.getDirectoryStructure(normalizePath(rootPath))
} }
@ -83,13 +90,16 @@ export async function renameNode(node: NotesTreeNode, newName: string): Promise<
return { path: `${parentDir}/${safeName}`, name: safeName } return { path: `${parentDir}/${safeName}`, name: safeName }
} }
// Function overloads for type safety
export async function uploadNotes(files: File[], targetPath: string): Promise<UploadResult>
export async function uploadNotes(entries: FileEntryData[], targetPath: string): Promise<UploadResult>
// Implementation signature
export async function uploadNotes( export async function uploadNotes(
files: File[] | Array<{ fullPath: string; isFile: boolean; isDirectory: boolean; systemPath: string }>, filesOrEntries: File[] | FileEntryData[],
targetPath: string targetPath: string
): Promise<UploadResult> { ): Promise<UploadResult> {
const basePath = normalizePath(targetPath) if (filesOrEntries.length === 0) {
if (files.length === 0) {
return { return {
uploadedNodes: [], uploadedNodes: [],
totalFiles: 0, totalFiles: 0,
@ -99,23 +109,26 @@ export async function uploadNotes(
} }
} }
const firstItem = files[0] // Check if we're dealing with FileEntryData by looking at the first item
const isEntryDataList = const firstItem = filesOrEntries[0]
typeof firstItem === 'object' && 'fullPath' in firstItem && 'systemPath' in firstItem && 'isFile' in firstItem if ('fullPath' in firstItem && 'systemPath' in firstItem) {
return uploadNotesFromEntries(filesOrEntries as FileEntryData[], targetPath)
if (isEntryDataList) {
const entries = files as Array<{ fullPath: string; isFile: boolean; isDirectory: boolean; systemPath: string }>
return uploadNotesRecursive(entries, targetPath)
} }
// Legacy approach: File objects (for browser File API compatibility) return uploadNotesFromFiles(filesOrEntries as File[], targetPath)
const fileList = files as File[] }
const totalFiles = fileList.length
/**
* Upload notes from File objects (browser File API)
*/
async function uploadNotesFromFiles(files: File[], targetPath: string): Promise<UploadResult> {
const basePath = normalizePath(targetPath)
const totalFiles = files.length
try { try {
const filePaths: string[] = [] const filePaths: string[] = []
for (const file of fileList) { for (const file of files) {
const filePath = window.api.file.getPathForFile(file) const filePath = window.api.file.getPathForFile(file)
if (filePath) { if (filePath) {
@ -156,7 +169,7 @@ export async function uploadNotes(
folderCount: result.folderCount folderCount: result.folderCount
} }
} catch (error) { } catch (error) {
logger.error('Legacy file upload failed:', error as Error) logger.error('File upload failed:', error as Error)
return { return {
uploadedNodes: [], uploadedNodes: [],
totalFiles, totalFiles,
@ -167,14 +180,18 @@ export async function uploadNotes(
} }
} }
/**
* Upload notes from FileEntryData (drag-and-drop with directory structure)
*/
async function uploadNotesFromEntries(entries: FileEntryData[], targetPath: string): Promise<UploadResult> {
return uploadNotesRecursive(entries, targetPath)
}
/** /**
* Recursive upload for drag-and-drop with fullPath preserved (VS Code approach) * Recursive upload for drag-and-drop with fullPath preserved (VS Code approach)
* Uses batch processing for better performance * Uses batch processing for better performance
*/ */
async function uploadNotesRecursive( async function uploadNotesRecursive(entryDataList: FileEntryData[], targetPath: string): Promise<UploadResult> {
entryDataList: Array<{ fullPath: string; isFile: boolean; isDirectory: boolean; systemPath: string }>,
targetPath: string
): Promise<UploadResult> {
const basePath = normalizePath(targetPath) const basePath = normalizePath(targetPath)
try { try {