Fix/newNode (#9727)

* feat: enhance note saving functionality with immediate cache invalidation

* fix: improve file name handling and localization updates

* feat: implement multi-level note sorting and enhance state management

- Introduced sorting options for notes by name, update time, and creation time, allowing users to sort notes in ascending and descending order.
- Updated NotesPage and NotesSidebar components to handle sorting functionality.
- Enhanced Redux store to manage the sorting type, improving state management for note organization.
- Refactored related services to support recursive sorting logic, ensuring a consistent user experience.

* feat(i18n): add new file upload messages for multiple languages

* fix(styles): adjust padding in richtext.scss to accommodate scrollbar

* style(NotesSidebar): add border-top-left-radius to enhance sidebar aesthetics

* feat(RichEditPopup): add isFullWidth prop to enhance popup layout

* feat(RichEditPopup): disable keyboard interaction for improved user experience

* feat(NotesPage): integrate sorting after node deletion and movement

- Added sorting functionality to be triggered after deleting or moving nodes, ensuring notes are organized immediately.
- Updated dependencies in useCallback hooks to include sortType for consistent behavior across operations.

* feat(NotesService): update initWorkSpace and sortAllLevels to accept sortType

- Modified initWorkSpace to include sortType for improved note organization during initialization.
- Enhanced sortAllLevels to optionally accept a tree parameter, allowing for more flexible sorting operations.
- Updated NotesPage to utilize the new parameters, ensuring consistent sorting behavior across various actions.

* feat(NotesSidebar): implement in-place editing for note renaming

- Introduced a new hook, useInPlaceEdit, to manage in-place editing of note names, enhancing user experience during renaming.
- Updated the NotesSidebar component to utilize this hook, streamlining the editing process and improving state management.
- Removed redundant state variables related to editing, simplifying the component's logic.

* refactor(NotesPage): remove commented code for clarity

- Removed a comment regarding folder selection behavior to streamline the code and improve readability.
- This change does not affect functionality but enhances the overall code quality.

* feat(NotesSettings): update initWorkSpace to include default sort type

- Modified initWorkSpace calls in NotesSettings to accept a default sort type of 'sort_a2z', ensuring consistent note organization during path updates and resets.
- This change enhances the initialization process by applying a predefined sorting method.
This commit is contained in:
SuYao 2025-08-31 21:53:53 +08:00 committed by GitHub
parent bf23c5b209
commit ce4cad67a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 198 additions and 80 deletions

View File

@ -311,8 +311,7 @@ export async function scanDir(dirPath: string, depth = 0, basePath?: string): Pr
*/ */
export function getName(baseDir: string, fileName: string, isFile: boolean): string { export function getName(baseDir: string, fileName: string, isFile: boolean): string {
// 首先清理文件名 // 首先清理文件名
const sanitizedName = sanitizeFilename(fileName) const baseName = sanitizeFilename(fileName)
const baseName = sanitizedName.replace(/\d+$/, '')
let candidate = isFile ? baseName + '.md' : baseName let candidate = isFile ? baseName + '.md' : baseName
let counter = 1 let counter = 1

View File

@ -1,5 +1,6 @@
.tiptap { .tiptap {
padding: 12px 60px; // 预留5px给scrollbar
padding: 12px 55px 12px 60px;
outline: none; outline: none;
min-height: 120px; min-height: 120px;
overflow-wrap: break-word; overflow-wrap: break-word;

View File

@ -97,6 +97,7 @@ const PopupContainer: React.FC<Props> = ({
afterClose={onClose} afterClose={onClose}
afterOpenChange={handleAfterOpenChange} afterOpenChange={handleAfterOpenChange}
maskClosable={false} maskClosable={false}
keyboard={false}
centered> centered>
<EditorContainer> <EditorContainer>
<RichEditor <RichEditor
@ -108,6 +109,7 @@ const PopupContainer: React.FC<Props> = ({
onCommandsReady={handleCommandsReady} onCommandsReady={handleCommandsReady}
minHeight={300} minHeight={300}
maxHeight={500} maxHeight={500}
isFullWidth={true}
className="rich-edit-popup-editor" className="rich-edit-popup-editor"
/> />
</EditorContainer> </EditorContainer>

View File

@ -1615,9 +1615,12 @@
"new_folder": "New Folder", "new_folder": "New Folder",
"new_note": "Create a new note", "new_note": "Create a new note",
"no_content_to_copy": "No content to copy", "no_content_to_copy": "No content to copy",
"no_file_selected": "Please select the file to upload",
"only_markdown": "Only Markdown files are supported", "only_markdown": "Only Markdown files are supported",
"only_one_file_allowed": "Only one file can be uploaded",
"open_folder": "Open an external folder", "open_folder": "Open an external folder",
"rename": "Rename", "rename": "Rename",
"rename_changed": "Due to security policies, the filename has been changed from {{original}} to {{final}}",
"save": "Save to Notes", "save": "Save to Notes",
"settings": { "settings": {
"data": { "data": {

View File

@ -1615,9 +1615,12 @@
"new_folder": "新しいフォルダーを作成する", "new_folder": "新しいフォルダーを作成する",
"new_note": "新規ノート作成", "new_note": "新規ノート作成",
"no_content_to_copy": "コピーするコンテンツはありません", "no_content_to_copy": "コピーするコンテンツはありません",
"no_file_selected": "アップロードするファイルを選択してください",
"only_markdown": "Markdown ファイルのみをアップロードできます", "only_markdown": "Markdown ファイルのみをアップロードできます",
"only_one_file_allowed": "アップロードできるファイルは1つだけです",
"open_folder": "外部フォルダーを開きます", "open_folder": "外部フォルダーを開きます",
"rename": "名前の変更", "rename": "名前の変更",
"rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました",
"save": "メモに保存する", "save": "メモに保存する",
"settings": { "settings": {
"data": { "data": {

View File

@ -1615,9 +1615,12 @@
"new_folder": "Новая папка", "new_folder": "Новая папка",
"new_note": "Создать заметку", "new_note": "Создать заметку",
"no_content_to_copy": "Нет контента для копирования", "no_content_to_copy": "Нет контента для копирования",
"no_file_selected": "Пожалуйста, выберите файл для загрузки",
"only_markdown": "Только Markdown", "only_markdown": "Только Markdown",
"only_one_file_allowed": "Можно загрузить только один файл",
"open_folder": "Откройте внешнюю папку", "open_folder": "Откройте внешнюю папку",
"rename": "переименовать", "rename": "переименовать",
"rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}",
"save": "Сохранить в заметки", "save": "Сохранить в заметки",
"settings": { "settings": {
"data": { "data": {

View File

@ -1615,9 +1615,12 @@
"new_folder": "新建文件夹", "new_folder": "新建文件夹",
"new_note": "新建笔记", "new_note": "新建笔记",
"no_content_to_copy": "没有内容可复制", "no_content_to_copy": "没有内容可复制",
"no_file_selected": "请选择要上传的文件",
"only_markdown": "仅支持 Markdown 格式", "only_markdown": "仅支持 Markdown 格式",
"only_one_file_allowed": "只能上传一个文件",
"open_folder": "打开外部文件夹", "open_folder": "打开外部文件夹",
"rename": "重命名", "rename": "重命名",
"rename_changed": "由于安全策略,文件名已从 {{original}} 更改为 {{final}}",
"save": "保存到笔记", "save": "保存到笔记",
"settings": { "settings": {
"data": { "data": {

View File

@ -1615,9 +1615,12 @@
"new_folder": "新建文件夾", "new_folder": "新建文件夾",
"new_note": "新建筆記", "new_note": "新建筆記",
"no_content_to_copy": "沒有內容可複制", "no_content_to_copy": "沒有內容可複制",
"no_file_selected": "請選擇要上傳的文件",
"only_markdown": "僅支援 Markdown 格式", "only_markdown": "僅支援 Markdown 格式",
"only_one_file_allowed": "只能上傳一個文件",
"open_folder": "打開外部文件夾", "open_folder": "打開外部文件夾",
"rename": "重命名", "rename": "重命名",
"rename_changed": "由於安全策略,文件名已從 {{original}} 更改為 {{final}}",
"save": "儲存到筆記", "save": "儲存到筆記",
"settings": { "settings": {
"data": { "data": {

View File

@ -16,7 +16,7 @@ import {
} from '@renderer/services/NotesService' } from '@renderer/services/NotesService'
import { getNotesTree, isParentNode, updateNodeInTree } from '@renderer/services/NotesTreeService' import { getNotesTree, isParentNode, updateNodeInTree } from '@renderer/services/NotesTreeService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { selectActiveFilePath, setActiveFilePath } from '@renderer/store/note' import { selectActiveFilePath, selectSortType, setActiveFilePath, setSortType } from '@renderer/store/note'
import { NotesSortType, NotesTreeNode } from '@renderer/types/note' import { NotesSortType, NotesTreeNode } from '@renderer/types/note'
import { FileChangeEvent } from '@shared/config/types' import { FileChangeEvent } from '@shared/config/types'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
@ -37,6 +37,7 @@ const NotesPage: FC = () => {
const { showWorkspace } = useSettings() const { showWorkspace } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const activeFilePath = useAppSelector(selectActiveFilePath) const activeFilePath = useAppSelector(selectActiveFilePath)
const sortType = useAppSelector(selectSortType)
const { settings, notesPath, updateNotesPath } = useNotesSettings() const { settings, notesPath, updateNotesPath } = useNotesSettings()
// 混合策略useLiveQuery用于笔记树React Query用于文件内容 // 混合策略useLiveQuery用于笔记树React Query用于文件内容
@ -53,6 +54,8 @@ const NotesPage: FC = () => {
const isEditorInitialized = useRef(false) const isEditorInitialized = useRef(false)
const lastContentRef = useRef<string>('') const lastContentRef = useRef<string>('')
const isInitialSortApplied = useRef(false) const isInitialSortApplied = useRef(false)
const isRenamingRef = useRef(false)
const isCreatingNoteRef = useRef(false)
useEffect(() => { useEffect(() => {
const updateCharCount = () => { const updateCharCount = () => {
@ -131,7 +134,7 @@ const NotesPage: FC = () => {
async function applyInitialSort() { async function applyInitialSort() {
if (notesTree.length > 0 && !isInitialSortApplied.current) { if (notesTree.length > 0 && !isInitialSortApplied.current) {
try { try {
await sortAllLevels('sort_a2z') await sortAllLevels(sortType)
isInitialSortApplied.current = true isInitialSortApplied.current = true
} catch (error) { } catch (error) {
logger.error('Failed to apply initial sorting:', error as Error) logger.error('Failed to apply initial sorting:', error as Error)
@ -140,14 +143,21 @@ const NotesPage: FC = () => {
} }
applyInitialSort() applyInitialSort()
}, [notesTree.length]) }, [notesTree.length, sortType])
// 处理树同步时的状态管理 // 处理树同步时的状态管理
useEffect(() => { useEffect(() => {
if (notesTree.length === 0) return if (notesTree.length === 0) return
// 如果有activeFilePath但找不到对应节点清空选择 // 如果有activeFilePath但找不到对应节点清空选择
if (activeFilePath && !activeNode) { // 但要排除正在同步树结构、重命名或创建笔记的情况,避免在这些操作中误清空
if (
activeFilePath &&
!activeNode &&
!isSyncingTreeRef.current &&
!isRenamingRef.current &&
!isCreatingNoteRef.current
) {
dispatch(setActiveFilePath(undefined)) dispatch(setActiveFilePath(undefined))
} }
}, [notesTree, activeFilePath, activeNode, dispatch]) }, [notesTree, activeFilePath, activeNode, dispatch])
@ -191,7 +201,7 @@ const NotesPage: FC = () => {
invalidateFileContent(filePath) invalidateFileContent(filePath)
} }
} else { } else {
await initWorkSpace(notesPath) await initWorkSpace(notesPath, sortType)
} }
break break
} }
@ -210,7 +220,7 @@ const NotesPage: FC = () => {
// 重新同步数据库useLiveQuery会自动响应数据库变化 // 重新同步数据库useLiveQuery会自动响应数据库变化
try { try {
await initWorkSpace(notesPath) await initWorkSpace(notesPath, sortType)
} catch (error) { } catch (error) {
logger.error('Failed to sync database:', error as Error) logger.error('Failed to sync database:', error as Error)
} finally { } finally {
@ -264,7 +274,8 @@ const NotesPage: FC = () => {
dispatch, dispatch,
currentContent, currentContent,
debouncedSave, debouncedSave,
saveCurrentNote saveCurrentNote,
sortType
]) ])
useEffect(() => { useEffect(() => {
@ -273,7 +284,7 @@ const NotesPage: FC = () => {
// 标记编辑器已初始化 // 标记编辑器已初始化
isEditorInitialized.current = true isEditorInitialized.current = true
} }
}, [currentContent]) }, [currentContent, activeFilePath])
// 切换文件时重置编辑器初始化状态并兜底保存 // 切换文件时重置编辑器初始化状态并兜底保存
useEffect(() => { useEffect(() => {
@ -319,17 +330,27 @@ const NotesPage: FC = () => {
const handleCreateNote = useCallback( const handleCreateNote = useCallback(
async (name: string) => { async (name: string) => {
try { try {
isCreatingNoteRef.current = true
const targetPath = getTargetFolderPath() const targetPath = getTargetFolderPath()
if (!targetPath) { if (!targetPath) {
throw new Error('No folder path selected') throw new Error('No folder path selected')
} }
const newNote = await createNote(name, '', targetPath) const newNote = await createNote(name, '', targetPath)
dispatch(setActiveFilePath(newNote.externalPath)) dispatch(setActiveFilePath(newNote.externalPath))
setSelectedFolderId(null)
await sortAllLevels(sortType)
} catch (error) { } catch (error) {
logger.error('Failed to create note:', error as Error) logger.error('Failed to create note:', error as Error)
} finally {
// 延迟重置标志,给数据库同步一些时间
setTimeout(() => {
isCreatingNoteRef.current = false
}, 500)
} }
}, },
[dispatch, getTargetFolderPath] [dispatch, getTargetFolderPath, sortType]
) )
// 切换展开状态 // 切换展开状态
@ -410,10 +431,7 @@ const NotesPage: FC = () => {
logger.error('Failed to load note:', error as Error) logger.error('Failed to load note:', error as Error)
} }
} else if (node.type === 'folder') { } else if (node.type === 'folder') {
// 设置选中的文件夹,同时清除活动文件
setSelectedFolderId(node.id) setSelectedFolderId(node.id)
// 清除活动文件状态,这样文件的高亮会被清除
dispatch(setActiveFilePath(undefined))
await handleToggleExpanded(node.id) await handleToggleExpanded(node.id)
} }
}, },
@ -432,6 +450,7 @@ const NotesPage: FC = () => {
(nodeToDelete.externalPath === activeFilePath || isParentNode(notesTree, nodeId, activeNode?.id || '')) (nodeToDelete.externalPath === activeFilePath || isParentNode(notesTree, nodeId, activeNode?.id || ''))
await deleteNode(nodeId) await deleteNode(nodeId)
await sortAllLevels(sortType)
// 如果删除的是当前活动节点或其父节点,清空编辑器 // 如果删除的是当前活动节点或其父节点,清空编辑器
if (isActiveNodeOrParent) { if (isActiveNodeOrParent) {
@ -444,24 +463,47 @@ const NotesPage: FC = () => {
logger.error('Failed to delete node:', error as Error) logger.error('Failed to delete node:', error as Error)
} }
}, },
[activeFilePath, activeNode, notesTree, dispatch, findNodeById] [findNodeById, notesTree, activeFilePath, activeNode?.id, sortType, dispatch]
) )
// 重命名节点 // 重命名节点
const handleRenameNode = useCallback( const handleRenameNode = useCallback(
async (nodeId: string, newName: string) => { async (nodeId: string, newName: string) => {
try { try {
isRenamingRef.current = true
const tree = await getNotesTree() const tree = await getNotesTree()
const node = findNodeById(tree, nodeId) const node = findNodeById(tree, nodeId)
if (node && node.name !== newName) { if (node && node.name !== newName) {
await renameNode(nodeId, newName) const oldExternalPath = node.externalPath
const renamedNode = await renameNode(nodeId, newName)
if (renamedNode.type === 'file' && activeFilePath === oldExternalPath) {
dispatch(setActiveFilePath(renamedNode.externalPath))
} else if (
renamedNode.type === 'folder' &&
activeFilePath &&
activeFilePath.startsWith(oldExternalPath + '/')
) {
const relativePath = activeFilePath.substring(oldExternalPath.length)
const newFilePath = renamedNode.externalPath + relativePath
dispatch(setActiveFilePath(newFilePath))
}
await sortAllLevels(sortType)
if (renamedNode.name !== newName) {
window.message.info(t('notes.rename_changed', { original: newName, final: renamedNode.name }))
}
} }
} catch (error) { } catch (error) {
logger.error('Failed to rename node:', error as Error) logger.error('Failed to rename node:', error as Error)
} finally {
setTimeout(() => {
isRenamingRef.current = false
}, 500)
} }
}, },
[findNodeById] [activeFilePath, dispatch, findNodeById, sortType, t]
) )
// 处理文件上传 // 处理文件上传
@ -507,22 +549,28 @@ const NotesPage: FC = () => {
async (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => { async (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => {
try { try {
await moveNode(sourceNodeId, targetNodeId, position) await moveNode(sourceNodeId, targetNodeId, position)
await sortAllLevels(sortType)
} catch (error) { } catch (error) {
logger.error('Failed to move nodes:', error as Error) logger.error('Failed to move nodes:', error as Error)
} }
}, },
[] [sortType]
) )
// 处理节点排序 // 处理节点排序
const handleSortNodes = useCallback(async (sortType: NotesSortType) => { const handleSortNodes = useCallback(
try { async (newSortType: NotesSortType) => {
await sortAllLevels(sortType) try {
} catch (error) { // 更新Redux中的排序类型
logger.error('Failed to sort notes:', error as Error) dispatch(setSortType(newSortType))
throw error await sortAllLevels(newSortType)
} } catch (error) {
}, []) logger.error('Failed to sort notes:', error as Error)
throw error
}
},
[dispatch]
)
const getCurrentNoteContent = useCallback(() => { const getCurrentNoteContent = useCallback(() => {
if (settings.defaultEditMode === 'source') { if (settings.defaultEditMode === 'source') {

View File

@ -2,11 +2,14 @@ import { loggerService } from '@logger'
import { DeleteIcon } from '@renderer/components/Icons' import { DeleteIcon } from '@renderer/components/Icons'
import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup' import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
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 { useAppSelector } from '@renderer/store'
import { selectSortType } from '@renderer/store/note'
import { NotesSortType, NotesTreeNode } from '@renderer/types/note' import { NotesSortType, NotesTreeNode } from '@renderer/types/note'
import { Dropdown, Input, MenuProps } from 'antd' import { Dropdown, Input, InputRef, MenuProps } from 'antd'
import { import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
@ -19,7 +22,7 @@ import {
Star, Star,
StarOff StarOff
} from 'lucide-react' } from 'lucide-react'
import { FC, useCallback, useMemo, useRef, useState } from 'react' import { FC, Ref, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -57,8 +60,8 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
const { bases } = useKnowledgeBases() const { bases } = useKnowledgeBases()
const { activeNode } = useActiveNode(notesTree) const { activeNode } = useActiveNode(notesTree)
const sortType = useAppSelector(selectSortType)
const [editingNodeId, setEditingNodeId] = useState<string | null>(null) const [editingNodeId, setEditingNodeId] = useState<string | null>(null)
const [editingName, setEditingName] = useState('')
const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null) const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null)
const [dragOverNodeId, setDragOverNodeId] = useState<string | null>(null) const [dragOverNodeId, setDragOverNodeId] = useState<string | null>(null)
const [dragPosition, setDragPosition] = useState<'before' | 'inside' | 'after'>('inside') const [dragPosition, setDragPosition] = useState<'before' | 'inside' | 'after'>('inside')
@ -66,8 +69,56 @@ 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 [sortType, setSortType] = useState<NotesSortType>('sort_a2z')
const dragNodeRef = useRef<HTMLDivElement | null>(null) const dragNodeRef = useRef<HTMLDivElement | null>(null)
const scrollbarRef = useRef<any>(null)
const inPlaceEdit = useInPlaceEdit({
onSave: (newName: string) => {
if (editingNodeId && newName) {
onRenameNode(editingNodeId, newName)
logger.debug(`Renamed node ${editingNodeId} to "${newName}"`)
}
setEditingNodeId(null)
},
onCancel: () => {
setEditingNodeId(null)
}
})
// 滚动到活动节点
useEffect(() => {
if (activeNode?.id && !isShowStarred && !isShowSearch && scrollbarRef.current) {
// 延迟一下确保DOM已更新
setTimeout(() => {
const scrollContainer = scrollbarRef.current as HTMLElement
if (scrollContainer) {
const activeElement = scrollContainer.querySelector(`[data-node-id="${activeNode.id}"]`) as HTMLElement
if (activeElement) {
// 获取元素相对于滚动容器的位置
const containerHeight = scrollContainer.clientHeight
const elementOffsetTop = activeElement.offsetTop
const elementHeight = activeElement.offsetHeight
const currentScrollTop = scrollContainer.scrollTop
// 检查元素是否在可视区域内
const elementTop = elementOffsetTop
const elementBottom = elementOffsetTop + elementHeight
const viewTop = currentScrollTop
const viewBottom = currentScrollTop + containerHeight
// 如果元素不在可视区域内,滚动到中心位置
if (elementTop < viewTop || elementBottom > viewBottom) {
const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2
scrollContainer.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
})
}
}
}
}, 200)
}
}, [activeNode?.id, isShowStarred, isShowSearch])
const handleCreateFolder = useCallback(() => { const handleCreateFolder = useCallback(() => {
onCreateFolder(t('notes.untitled_folder')) onCreateFolder(t('notes.untitled_folder'))
@ -79,30 +130,18 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const handleSelectSortType = useCallback( const handleSelectSortType = useCallback(
(selectedSortType: NotesSortType) => { (selectedSortType: NotesSortType) => {
setSortType(selectedSortType)
onSortNodes(selectedSortType) onSortNodes(selectedSortType)
}, },
[onSortNodes] [onSortNodes]
) )
const handleStartEdit = useCallback((node: NotesTreeNode) => { const handleStartEdit = useCallback(
setEditingNodeId(node.id) (node: NotesTreeNode) => {
setEditingName(node.name) setEditingNodeId(node.id)
}, []) inPlaceEdit.startEdit(node.name)
},
const handleFinishEdit = useCallback(() => { [inPlaceEdit]
if (editingNodeId && editingName.trim()) { )
onRenameNode(editingNodeId, editingName.trim())
}
setEditingNodeId(null)
setEditingName('')
logger.debug(`Renamed node ${editingNodeId} to "${editingName.trim()}"`)
}, [editingNodeId, editingName, onRenameNode])
const handleCancelEdit = useCallback(() => {
setEditingNodeId(null)
setEditingName('')
}, [])
const handleDeleteNode = useCallback( const handleDeleteNode = useCallback(
(node: NotesTreeNode) => { (node: NotesTreeNode) => {
@ -306,8 +345,10 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const renderTreeNode = useCallback( const renderTreeNode = useCallback(
(node: NotesTreeNode, depth: number = 0) => { (node: NotesTreeNode, depth: number = 0) => {
const isActive = node.id === activeNode?.id || (node.type === 'folder' && node.id === selectedFolderId) const isActive = selectedFolderId
const isEditing = editingNodeId === node.id ? node.type === 'folder' && node.id === selectedFolderId
: node.id === activeNode?.id
const isEditing = editingNodeId === node.id && inPlaceEdit.isEditing
const hasChildren = node.children && node.children.length > 0 const hasChildren = node.children && node.children.length > 0
const isDragging = draggedNodeId === node.id const isDragging = draggedNodeId === node.id
const isDragOver = dragOverNodeId === node.id const isDragOver = dragOverNodeId === node.id
@ -328,6 +369,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
isDragInside={isDragInside} isDragInside={isDragInside}
isDragAfter={isDragAfter} isDragAfter={isDragAfter}
draggable={!isEditing} draggable={!isEditing}
data-node-id={node.id}
onDragStart={(e) => handleDragStart(e, node)} onDragStart={(e) => handleDragStart(e, node)}
onDragOver={(e) => handleDragOver(e, node)} onDragOver={(e) => handleDragOver(e, node)}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@ -361,15 +403,13 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
{isEditing ? ( {isEditing ? (
<EditInput <EditInput
value={editingName} ref={inPlaceEdit.inputRef as Ref<InputRef>}
onChange={(e) => setEditingName(e.target.value)} value={inPlaceEdit.editValue}
onPressEnter={handleFinishEdit} onChange={inPlaceEdit.handleInputChange}
onBlur={handleFinishEdit} onPressEnter={inPlaceEdit.saveEdit}
onKeyDown={(e) => { onBlur={inPlaceEdit.saveEdit}
if (e.key === 'Escape') { onKeyDown={inPlaceEdit.handleKeyDown}
handleCancelEdit() onClick={(e) => e.stopPropagation()}
}
}}
autoFocus autoFocus
size="small" size="small"
/> />
@ -388,24 +428,27 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
) )
}, },
[ [
activeNode,
selectedFolderId, selectedFolderId,
activeNode?.id,
editingNodeId, editingNodeId,
editingName, inPlaceEdit.isEditing,
inPlaceEdit.inputRef,
inPlaceEdit.editValue,
inPlaceEdit.handleInputChange,
inPlaceEdit.saveEdit,
inPlaceEdit.handleKeyDown,
draggedNodeId, draggedNodeId,
dragOverNodeId, dragOverNodeId,
dragPosition, dragPosition,
onSelectNode, getMenuItems,
onToggleExpanded, handleDragLeave,
handleFinishEdit, handleDragEnd,
handleCancelEdit, t,
handleDragStart, handleDragStart,
handleDragOver, handleDragOver,
handleDragLeave,
handleDrop, handleDrop,
handleDragEnd, onSelectNode,
getMenuItems, onToggleExpanded
t
] ]
) )
@ -451,7 +494,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
/> />
<NotesTreeContainer> <NotesTreeContainer>
<StyledScrollbar> <StyledScrollbar ref={scrollbarRef}>
<TreeContent> <TreeContent>
{filteredTree.map((node) => renderTreeNode(node))} {filteredTree.map((node) => renderTreeNode(node))}
{!isShowStarred && !isShowSearch && ( {!isShowStarred && !isShowSearch && (
@ -480,6 +523,7 @@ const SidebarContainer = styled.div`
height: 100vh; height: 100vh;
background-color: var(--color-background); background-color: var(--color-background);
border-right: 1px solid var(--color-border); border-right: 1px solid var(--color-border);
border-top-left-radius: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;

View File

@ -70,7 +70,7 @@ const NotesSettings: FC = () => {
} }
updateNotesPath(tempPath) updateNotesPath(tempPath)
initWorkSpace(tempPath) initWorkSpace(tempPath, 'sort_a2z')
window.message.success(t('notes.settings.data.path_updated')) window.message.success(t('notes.settings.data.path_updated'))
} catch (error) { } catch (error) {
logger.error('Failed to apply notes path:', error as Error) logger.error('Failed to apply notes path:', error as Error)
@ -83,7 +83,7 @@ const NotesSettings: FC = () => {
const info = await window.api.getAppInfo() const info = await window.api.getAppInfo()
setTempPath(info.notesPath) setTempPath(info.notesPath)
updateNotesPath(info.notesPath) updateNotesPath(info.notesPath)
initWorkSpace(info.notesPath) initWorkSpace(info.notesPath, 'sort_a2z')
window.message.success(t('notes.settings.data.reset_to_default')) window.message.success(t('notes.settings.data.reset_to_default'))
} catch (error) { } catch (error) {
logger.error('Failed to reset to default:', error as Error) logger.error('Failed to reset to default:', error as Error)

View File

@ -22,9 +22,9 @@ const logger = loggerService.withContext('NotesService')
/** /**
* / * /
*/ */
export async function initWorkSpace(folderPath: string): Promise<void> { export async function initWorkSpace(folderPath: string, sortType: NotesSortType): Promise<void> {
const tree = await window.api.file.getDirectoryStructure(folderPath) const tree = await window.api.file.getDirectoryStructure(folderPath)
await db.notes_tree.put({ id: NOTES_TREE_ID, tree }) await sortAllLevels(sortType, tree)
} }
/** /**
@ -326,9 +326,11 @@ function getSortFunction(sortType: NotesSortType): (a: NotesTreeNode, b: NotesTr
/** /**
* *
*/ */
export async function sortAllLevels(sortType: NotesSortType): Promise<void> { export async function sortAllLevels(sortType: NotesSortType, tree?: NotesTreeNode[]): Promise<void> {
try { try {
const tree = await getNotesTree() if (!tree) {
tree = await getNotesTree()
}
sortNodesArray(tree, sortType) sortNodesArray(tree, sortType)
recursiveSortNodes(tree, sortType) recursiveSortNodes(tree, sortType)
await db.notes_tree.put({ id: NOTES_TREE_ID, tree }) await db.notes_tree.put({ id: NOTES_TREE_ID, tree })

View File

@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@renderer/store/index' import { RootState } from '@renderer/store/index'
import { EditorView } from '@renderer/types' import { EditorView } from '@renderer/types'
import { NotesSortType } from '@renderer/types/note'
export interface NotesSettings { export interface NotesSettings {
isFullWidth: boolean isFullWidth: boolean
@ -15,6 +16,7 @@ export interface NoteState {
activeFilePath: string | undefined // 使用文件路径而不是nodeId activeFilePath: string | undefined // 使用文件路径而不是nodeId
settings: NotesSettings settings: NotesSettings
notesPath: string notesPath: string
sortType: NotesSortType
} }
export const initialState: NoteState = { export const initialState: NoteState = {
@ -27,7 +29,8 @@ export const initialState: NoteState = {
defaultEditMode: 'preview', defaultEditMode: 'preview',
showTabStatus: true showTabStatus: true
}, },
notesPath: '' notesPath: '',
sortType: 'sort_a2z'
} }
const noteSlice = createSlice({ const noteSlice = createSlice({
@ -45,15 +48,19 @@ const noteSlice = createSlice({
}, },
setNotesPath: (state, action: PayloadAction<string>) => { setNotesPath: (state, action: PayloadAction<string>) => {
state.notesPath = action.payload state.notesPath = action.payload
},
setSortType: (state, action: PayloadAction<NotesSortType>) => {
state.sortType = action.payload
} }
} }
}) })
export const { setActiveNodeId, setActiveFilePath, updateNotesSettings, setNotesPath } = noteSlice.actions export const { setActiveNodeId, setActiveFilePath, updateNotesSettings, setNotesPath, setSortType } = noteSlice.actions
export const selectActiveNodeId = (state: RootState) => state.note.activeNodeId export const selectActiveNodeId = (state: RootState) => state.note.activeNodeId
export const selectActiveFilePath = (state: RootState) => state.note.activeFilePath export const selectActiveFilePath = (state: RootState) => state.note.activeFilePath
export const selectNotesSettings = (state: RootState) => state.note.settings export const selectNotesSettings = (state: RootState) => state.note.settings
export const selectNotesPath = (state: RootState) => state.note.notesPath export const selectNotesPath = (state: RootState) => state.note.notesPath
export const selectSortType = (state: RootState) => state.note.sortType
export default noteSlice.reducer export default noteSlice.reducer