From d68aafea15dcb8643627dc99971ca026cf7625b6 Mon Sep 17 00:00:00 2001 From: one Date: Fri, 12 Sep 2025 09:30:47 +0800 Subject: [PATCH 1/2] fix: infinite loops in KnowledgeBaseButton and MentionModelsButton (#10118) --- src/renderer/src/components/QuickPanel/provider.tsx | 4 ++-- src/renderer/src/components/QuickPanel/types.ts | 5 +---- src/renderer/src/components/QuickPanel/view.tsx | 10 ++++------ .../src/pages/home/Inputbar/KnowledgeBaseButton.tsx | 6 +++--- .../src/pages/home/Inputbar/MentionModelsButton.tsx | 13 ++++++------- 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/components/QuickPanel/provider.tsx b/src/renderer/src/components/QuickPanel/provider.tsx index 85224e7a39..57eae70ef2 100644 --- a/src/renderer/src/components/QuickPanel/provider.tsx +++ b/src/renderer/src/components/QuickPanel/provider.tsx @@ -61,7 +61,7 @@ export const QuickPanelProvider: React.FC = ({ children const close = useCallback( (action?: QuickPanelCloseAction, searchText?: string) => { setIsVisible(false) - onClose?.({ symbol, action, triggerInfo, searchText, item: {} as QuickPanelListItem, multiple: false }) + onClose?.({ action, searchText, item: {} as QuickPanelListItem, context: this }) clearTimer.current = setTimeout(() => { setList([]) @@ -73,7 +73,7 @@ export const QuickPanelProvider: React.FC = ({ children setTriggerInfo(undefined) }, 200) }, - [onClose, symbol, triggerInfo] + [onClose] ) useEffect(() => { diff --git a/src/renderer/src/components/QuickPanel/types.ts b/src/renderer/src/components/QuickPanel/types.ts index 030eb6f1f4..97e072dea0 100644 --- a/src/renderer/src/components/QuickPanel/types.ts +++ b/src/renderer/src/components/QuickPanel/types.ts @@ -8,13 +8,10 @@ export type QuickPanelTriggerInfo = { } export type QuickPanelCallBackOptions = { - symbol: string + context: QuickPanelContextType action: QuickPanelCloseAction item: QuickPanelListItem searchText?: string - /** 是否处于多选状态 */ - multiple?: boolean - triggerInfo?: QuickPanelTriggerInfo } export type QuickPanelOpenOptions = { diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 08878b8478..30955f96f3 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -222,11 +222,10 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { // 创建更新后的item对象用于回调 const updatedItem = { ...item, isSelected: newSelectedState } const quickPanelCallBackOptions: QuickPanelCallBackOptions = { - symbol: ctx.symbol, + context: ctx, action, item: updatedItem, - searchText: searchText, - multiple: ctx.multiple + searchText: searchText } ctx.beforeAction?.(quickPanelCallBackOptions) @@ -236,11 +235,10 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { } const quickPanelCallBackOptions: QuickPanelCallBackOptions = { - symbol: ctx.symbol, + context: ctx, action, item, - searchText: searchText, - multiple: ctx.multiple + searchText: searchText } ctx.beforeAction?.(quickPanelCallBackOptions) diff --git a/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx b/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx index d183114658..bdb1a45353 100644 --- a/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx @@ -64,14 +64,14 @@ const KnowledgeBaseButton: FC = ({ ref, selectedBases, onSelect, disabled description: t('settings.input.clear.knowledge_base'), icon: , isSelected: false, - action: () => { + action: ({ context: ctx }) => { onSelect([]) - quickPanel.close() + ctx.close() } }) return items - }, [knowledgeState.bases, t, selectedBases, handleBaseSelect, navigate, onSelect, quickPanel]) + }, [knowledgeState.bases, t, selectedBases, handleBaseSelect, navigate, onSelect]) const openQuickPanel = useCallback(() => { quickPanel.open({ diff --git a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx index 26d94f250d..b252de981d 100644 --- a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx @@ -202,7 +202,7 @@ const MentionModelsButton: FC = ({ icon: , alwaysVisible: true, isSelected: false, - action: () => { + action: ({ context: ctx }) => { onClearMentionModels() // 只有输入触发时才需要删除 @ 与搜索文本(未知搜索词,按光标就近删除) @@ -214,7 +214,7 @@ const MentionModelsButton: FC = ({ }) } - quickPanel.close() + ctx.close() } }) @@ -227,7 +227,6 @@ const MentionModelsButton: FC = ({ mentionedModels, onMentionModel, navigate, - quickPanel, onClearMentionModels, setText, removeAtSymbolAndText @@ -249,20 +248,20 @@ const MentionModelsButton: FC = ({ afterAction({ item }) { item.isSelected = !item.isSelected }, - onClose({ action, triggerInfo: closeTriggerInfo, searchText }) { + onClose({ action, searchText, context: ctx }) { // ESC关闭时的处理:删除 @ 和搜索文本 if (action === 'esc') { // 只有在输入触发且有模型选择动作时才删除@字符和搜索文本 if ( hasModelActionRef.current && - closeTriggerInfo?.type === 'input' && - closeTriggerInfo?.position !== undefined + ctx.triggerInfo?.type === 'input' && + ctx.triggerInfo?.position !== undefined ) { // 基于当前光标 + 搜索词精确定位并删除,position 仅作兜底 setText((currentText) => { const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement | null const caret = textArea ? (textArea.selectionStart ?? currentText.length) : currentText.length - return removeAtSymbolAndText(currentText, caret, searchText || '', closeTriggerInfo.position!) + return removeAtSymbolAndText(currentText, caret, searchText || '', ctx.triggerInfo?.position!) }) } } From 5f999d3c84eb616683a9ce478a821b11d66cb676 Mon Sep 17 00:00:00 2001 From: Pleasure1234 <3196812536@qq.com> Date: Fri, 12 Sep 2025 12:56:30 +0800 Subject: [PATCH 2/2] fix: improve notes sidebar functionality and i18n text updates (#10112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: improve notes sidebar functionality and i18n text updates - Update breadcrumb navigation with clickable folder navigation and proper overflow handling - Add file selection dialog for markdown imports alongside drag-and-drop - Improve button order in sidebar header (new note first, then new folder) - Update i18n text: "markdown" → ".md" for clarity and "star" → "favorite note" for consistency - Fix file upload logic for better parent node handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: code review --------- Co-authored-by: Claude --- src/main/utils/file.ts | 10 +- src/renderer/src/i18n/locales/en-us.json | 4 +- src/renderer/src/i18n/locales/zh-cn.json | 4 +- src/renderer/src/i18n/locales/zh-tw.json | 4 +- src/renderer/src/i18n/translate/el-gr.json | 4 +- src/renderer/src/i18n/translate/es-es.json | 4 +- src/renderer/src/i18n/translate/fr-fr.json | 4 +- src/renderer/src/i18n/translate/ja-jp.json | 4 +- src/renderer/src/i18n/translate/pt-pt.json | 4 +- src/renderer/src/i18n/translate/ru-ru.json | 4 +- src/renderer/src/pages/notes/HeaderNavbar.tsx | 117 +++++++++++++++++- src/renderer/src/pages/notes/NotesSidebar.tsx | 27 ++-- .../src/pages/notes/NotesSidebarHeader.tsx | 12 +- src/renderer/src/services/NotesService.ts | 30 +++-- 14 files changed, 179 insertions(+), 53 deletions(-) diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 97e87bf9ea..f9363a500e 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -398,11 +398,15 @@ export function validateFileName(fileName: string, platform = process.platform): * @returns 合法的文件名 */ export function checkName(fileName: string): string { - const validation = validateFileName(fileName) + const baseName = path.basename(fileName) + const validation = validateFileName(baseName) if (!validation.valid) { - throw new Error(`Invalid file name: ${fileName}. ${validation.error}`) + // 自动清理非法字符,而不是抛出错误 + const sanitized = sanitizeFilename(baseName) + logger.warn(`File name contains invalid characters, auto-sanitized: "${baseName}" -> "${sanitized}"`) + return sanitized } - return fileName + return baseName } /** diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b4dc172bd2..066478b2f1 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1708,7 +1708,7 @@ "delete_confirm": "Are you sure you want to delete this {{type}}?", "delete_folder_confirm": "Are you sure you want to delete the folder \"{{name}}\" and all of its contents?", "delete_note_confirm": "Are you sure you want to delete the note \"{{name}}\"?", - "drop_markdown_hint": "Drop markdown files or folders here to import", + "drop_markdown_hint": "Drop .md files or folders here to import", "empty": "No notes available yet", "expand": "unfold", "export_failed": "Failed to export to knowledge base", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "Update time (oldest first)", "sort_updated_desc": "Update time (newest first)", "sort_z2a": "File name (Z-A)", - "star": "Favorite", + "star": "Favorite note", "starred_notes": "Collected notes", "title": "Notes", "unsaved_changes": "You have unsaved content, are you sure you want to leave?", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5b8cae2695..002284335b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1709,7 +1709,7 @@ "delete_confirm": "确定要删除这个{{type}}吗?", "delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?", "delete_note_confirm": "确定要删除笔记 \"{{name}}\" 吗?", - "drop_markdown_hint": "拖拽 Markdown 文件或目录到此处导入", + "drop_markdown_hint": "拖拽 .md 文件或目录到此处导入", "empty": "暂无笔记", "expand": "展开", "export_failed": "导出到知识库失败", @@ -1782,7 +1782,7 @@ "sort_updated_asc": "更新时间(从旧到新)", "sort_updated_desc": "更新时间(从新到旧)", "sort_z2a": "文件名(Z-A)", - "star": "收藏", + "star": "收藏笔记", "starred_notes": "收藏的笔记", "title": "笔记", "unsaved_changes": "你有未保存的内容,确定要离开吗?", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 40639664d2..753981be90 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1708,7 +1708,7 @@ "delete_confirm": "確定要刪除此 {{type}} 嗎?", "delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?", "delete_note_confirm": "確定要刪除筆記 \"{{name}}\" 嗎?", - "drop_markdown_hint": "拖拽 Markdown 文件或資料夾到此處導入", + "drop_markdown_hint": "拖拽 .md 文件或資料夾到此處導入", "empty": "暫無筆記", "expand": "展開", "export_failed": "匯出至知識庫失敗", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "更新時間(從舊到新)", "sort_updated_desc": "更新時間(從新到舊)", "sort_z2a": "文件名(Z-A)", - "star": "收藏", + "star": "收藏筆記", "starred_notes": "收藏的筆記", "title": "筆記", "unsaved_changes": "你有未儲存的內容,確定要離開嗎?", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index c757be180a..8734c28d1e 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1708,7 +1708,7 @@ "delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};", "delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;", "delete_note_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη σημείωση \"{{name}}\";;", - "drop_markdown_hint": "Σύρετε και αποθέστε αρχεία ή φακέλους Markdown εδώ για εισαγωγή", + "drop_markdown_hint": "Σύρετε και αποθέστε αρχεία ή φακέλους .md εδώ για εισαγωγή", "empty": "δεν υπάρχει σημείωση για τώρα", "expand": "να ανοίξει", "export_failed": "Εξαγωγή στη βάση γνώσης απέτυχε", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)", "sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)", "sort_z2a": "όνομα αρχείου (Z-A)", - "star": "Αποθήκευση", + "star": "Αγαπημένες σημειώσεις", "starred_notes": "Σημειώσεις συλλογής", "title": "σημειώσεις", "unsaved_changes": "Έχετε μη αποθηκευμένο περιεχόμενο, είστε βέβαιοι ότι θέλετε να φύγετε;", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 49119b7d22..a9d9174e17 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1708,7 +1708,7 @@ "delete_confirm": "¿Estás seguro de que deseas eliminar este {{type}}?", "delete_folder_confirm": "¿Está seguro de que desea eliminar la carpeta \"{{name}}\" y todo su contenido?", "delete_note_confirm": "¿Está seguro de que desea eliminar la nota \"{{name}}\"?", - "drop_markdown_hint": "Arrastre y suelte archivos o carpetas de Markdown aquí para importar", + "drop_markdown_hint": "Arrastre y suelte archivos o carpetas de .md aquí para importar", "empty": "Sin notas por el momento", "expand": "expandir", "export_failed": "Exportación a la base de conocimientos fallida", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)", "sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)", "sort_z2a": "Nombre de archivo (Z-A)", - "star": "Colección", + "star": "Notas guardadas", "starred_notes": "notas guardadas", "title": "notas", "unsaved_changes": "Tienes contenido no guardado, ¿estás seguro de que quieres salir?", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 90d26eb045..db0e259708 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1708,7 +1708,7 @@ "delete_confirm": "Êtes-vous sûr de vouloir supprimer ce {{type}} ?", "delete_folder_confirm": "Êtes-vous sûr de vouloir supprimer le dossier \"{{name}}\" et tout son contenu ?", "delete_note_confirm": "Êtes-vous sûr de vouloir supprimer la note \"{{name}}\" ?", - "drop_markdown_hint": "Déposez ici des fichiers ou dossiers Markdown pour les importer", + "drop_markdown_hint": "Déposez ici des fichiers ou dossiers .md pour les importer", "empty": "Aucune note pour le moment", "expand": "développer", "export_failed": "Échec de l'exportation vers la base de connaissances", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)", "sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)", "sort_z2a": "Nom de fichier (Z-A)", - "star": "Favori", + "star": "Notes enregistrées", "starred_notes": "notes de collection", "title": "notes", "unsaved_changes": "Vous avez des modifications non enregistrées, êtes-vous sûr de vouloir quitter ?", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index a65d6b4f8e..6b17d41b0e 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -1708,7 +1708,7 @@ "delete_confirm": "この{{type}}を本当に削除しますか?", "delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?", "delete_note_confirm": "メモ \"{{name}}\" を削除してもよろしいですか?", - "drop_markdown_hint": "Markdown ファイルまたはディレクトリをここにドラッグ&ドロップしてインポートしてください", + "drop_markdown_hint": ".md ファイルまたはディレクトリをここにドラッグ&ドロップしてインポートしてください", "empty": "暫無ノート", "expand": "展開", "export_failed": "知識ベースへのエクスポートに失敗しました", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "更新日時(古い順)", "sort_updated_desc": "更新日時(新しい順)", "sort_z2a": "ファイル名(Z-A)", - "star": "お気に入りに追加する", + "star": "お気に入りのノート", "starred_notes": "収集したノート", "title": "ノート", "unsaved_changes": "保存されていないコンテンツがあります。本当に離れますか?", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index fc0fea726e..b285ae92ed 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1708,7 +1708,7 @@ "delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?", "delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?", "delete_note_confirm": "Tem a certeza de que deseja eliminar a nota \"{{name}}\"?", - "drop_markdown_hint": "Arraste e solte arquivos ou pastas Markdown aqui para importar", + "drop_markdown_hint": "Arraste e solte arquivos ou pastas .md aqui para importar", "empty": "Ainda não existem notas", "expand": "expandir", "export_failed": "Falha ao exportar para a base de conhecimento", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)", "sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)", "sort_z2a": "Nome do arquivo (Z-A)", - "star": "coleções", + "star": "Notas favoritas", "starred_notes": "notas salvas", "title": "nota", "unsaved_changes": "Você tem conteúdo não salvo, tem certeza que deseja sair?", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index a9879dc962..dba8501c68 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -1708,7 +1708,7 @@ "delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?", "delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?", "delete_note_confirm": "Вы действительно хотите удалить заметку \"{{name}}\"?", - "drop_markdown_hint": "Перетащите сюда файлы или папки Markdown для импорта", + "drop_markdown_hint": "Перетащите сюда файлы или папки .md для импорта", "empty": "заметок пока нет", "expand": "развернуть", "export_failed": "Экспорт в базу знаний не выполнен", @@ -1781,7 +1781,7 @@ "sort_updated_asc": "Время обновления (от старого к новому)", "sort_updated_desc": "Время обновления (от нового к старому)", "sort_z2a": "Имя файла (Я-А)", - "star": "Сохранить", + "star": "Избранные заметки", "starred_notes": "Сохраненные заметки", "title": "заметки", "unsaved_changes": "Вы не сохранили содержимое. Вы уверены, что хотите уйти?", diff --git a/src/renderer/src/pages/notes/HeaderNavbar.tsx b/src/renderer/src/pages/notes/HeaderNavbar.tsx index 5f386f4b38..c9eb189302 100644 --- a/src/renderer/src/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/src/pages/notes/HeaderNavbar.tsx @@ -1,11 +1,13 @@ +import { BreadcrumbItem, Breadcrumbs } from '@heroui/react' import { loggerService } from '@logger' import { NavbarCenter, NavbarHeader, NavbarRight } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' import { useActiveNode } from '@renderer/hooks/useNotesQuery' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace' -import { findNodeInTree } from '@renderer/services/NotesTreeService' -import { Breadcrumb, BreadcrumbProps, Dropdown, Tooltip } from 'antd' +import { findNodeByPath, findNodeInTree, updateNodeInTree } from '@renderer/services/NotesTreeService' +import { NotesTreeNode } from '@types' +import { Dropdown, Tooltip } from 'antd' import { t } from 'i18next' import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' @@ -18,7 +20,9 @@ const logger = loggerService.withContext('HeaderNavbar') const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { const { showWorkspace, toggleShowWorkspace } = useShowWorkspace() const { activeNode } = useActiveNode(notesTree) - const [breadcrumbItems, setBreadcrumbItems] = useState['items']>([]) + const [breadcrumbItems, setBreadcrumbItems] = useState< + Array<{ key: string; title: string; treePath: string; isFolder: boolean }> + >([]) const { settings, updateSettings } = useNotesSettings() const canShowStarButton = activeNode?.type === 'file' && onToggleStar @@ -47,6 +51,40 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { } }, [getCurrentNoteContent]) + const handleBreadcrumbClick = useCallback( + async (item: { treePath: string; isFolder: boolean }) => { + if (item.isFolder && notesTree) { + try { + // 获取从根目录到点击目录的所有路径片段 + const pathParts = item.treePath.split('/').filter(Boolean) + const expandPromises: Promise[] = [] + + // 逐级展开从根到目标路径的所有文件夹 + for (let i = 0; i < pathParts.length; i++) { + const currentPath = '/' + pathParts.slice(0, i + 1).join('/') + const folderNode = findNodeByPath(notesTree, currentPath) + + if (folderNode && folderNode.type === 'folder' && !folderNode.expanded) { + expandPromises.push(updateNodeInTree(notesTree, folderNode.id, { expanded: true })) + } + } + + // 并行执行所有展开操作 + if (expandPromises.length > 0) { + await Promise.all(expandPromises) + logger.info('Expanded folder path from breadcrumb:', { + targetPath: item.treePath, + expandedCount: expandPromises.length + }) + } + } catch (error) { + logger.error('Failed to expand folder path from breadcrumb:', error as Error) + } + } + }, + [notesTree] + ) + const buildMenuItem = (item: any) => { if (item.type === 'divider') { return { type: 'divider' as const, key: item.key } @@ -106,9 +144,13 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { const pathParts = node.treePath.split('/').filter(Boolean) const items = pathParts.map((part, index) => { + const currentPath = '/' + pathParts.slice(0, index + 1).join('/') + const isLastItem = index === pathParts.length - 1 return { key: `path-${index}`, - title: part + title: part, + treePath: currentPath, + isFolder: !isLastItem || node.type === 'folder' } }) @@ -135,8 +177,20 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { )} - - + + + + {breadcrumbItems.map((item, index) => ( + + handleBreadcrumbClick(item)} + $clickable={item.isFolder && index < breadcrumbItems.length - 1}> + {item.title} + + + ))} + + {canShowStarButton && ( @@ -225,4 +279,55 @@ export const StarButton = styled.div` } ` +export const BreadcrumbsContainer = styled.div` + width: 100%; + overflow: hidden; + + /* 确保 HeroUI Breadcrumbs 组件保持在一行 */ + & > nav { + white-space: nowrap; + overflow: hidden; + } + + & ol { + flex-wrap: nowrap !important; + overflow: hidden; + display: flex; + align-items: center; + } + + & li { + flex-shrink: 1; + min-width: 0; + display: flex; + align-items: center; + } + + /* 确保分隔符不会与标题重叠 */ + & li:not(:last-child)::after { + flex-shrink: 0; + margin: 0 8px; + } +` + +export const BreadcrumbTitle = styled.span<{ $clickable?: boolean }>` + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + flex-shrink: 1; + min-width: 0; + + ${({ $clickable }) => + $clickable && + ` + cursor: pointer; + &:hover { + color: var(--color-primary); + text-decoration: underline; + } + `} +` + export default HeaderNavbar diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index 1de9ff8a28..c9803208c3 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -518,6 +518,25 @@ const NotesSidebar: FC = ({ [onUploadFiles] ) + const handleClickToSelectFiles = useCallback(() => { + const fileInput = document.createElement('input') + fileInput.type = 'file' + fileInput.multiple = true + fileInput.accept = '.md,.markdown' + fileInput.webkitdirectory = false + + fileInput.onchange = (e) => { + const target = e.target as HTMLInputElement + if (target.files && target.files.length > 0) { + const selectedFiles = Array.from(target.files) + onUploadFiles(selectedFiles) + } + fileInput.remove() + } + + fileInput.click() + }, [onUploadFiles]) + return ( { @@ -556,7 +575,7 @@ const NotesSidebar: FC = ({ - {t('notes.drop_markdown_hint')} + {t('notes.drop_markdown_hint')} @@ -718,12 +737,6 @@ const NodeName = styled.div` const EditInput = styled(Input)` flex: 1; font-size: 13px; - - .ant-input { - font-size: 13px; - padding: 2px 6px; - border: 0.5px solid var(--color-primary); - } ` const DragOverIndicator = styled.div` diff --git a/src/renderer/src/pages/notes/NotesSidebarHeader.tsx b/src/renderer/src/pages/notes/NotesSidebarHeader.tsx index 7f47ad14c7..f4d4e17e7d 100644 --- a/src/renderer/src/pages/notes/NotesSidebarHeader.tsx +++ b/src/renderer/src/pages/notes/NotesSidebarHeader.tsx @@ -69,18 +69,18 @@ const NotesSidebarHeader: FC = ({ {!isShowStarred && !isShowSearch && ( <> - - - - - - + + + + + + node.externalPath === targetFolderPath) || findNodeByExternalPath(tree, targetFolderPath) + + if (!parentNode) { + logger.debug(`Uploading file ${fileName} to root directory: ${targetFolderPath}`) + } } else { parentNode = createdFolders.get(originalDirPath) || null if (!parentNode) { @@ -696,22 +701,21 @@ async function uploadSingleFile( parentNode = findNodeByExternalPath(tree, originalDirPath) } } - } - // 如果找不到父节点,尝试通过 createdFolders 找到实际路径 - if (!parentNode && originalDirPath !== targetFolderPath) { - for (const [originalPath, createdNode] of createdFolders.entries()) { - if (originalPath === originalDirPath) { - parentNode = createdNode - actualDirPath = createdNode.externalPath - break + if (!parentNode) { + for (const [originalPath, createdNode] of createdFolders.entries()) { + if (originalPath === originalDirPath) { + parentNode = createdNode + actualDirPath = createdNode.externalPath + break + } } } - } - if (!parentNode) { - logger.error(`Cannot upload file ${fileName}: parent node not found for path ${originalDirPath}`) - return null + if (!parentNode) { + logger.error(`Cannot upload file ${fileName}: parent node not found for path ${originalDirPath}`) + return null + } } const { safeName, exists } = await window.api.file.checkFileName(actualDirPath, nameWithoutExt, true)