mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 05:11:24 +08:00
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:
parent
bf23c5b209
commit
ce4cad67a6
@ -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 {
|
||||
// 首先清理文件名
|
||||
const sanitizedName = sanitizeFilename(fileName)
|
||||
const baseName = sanitizedName.replace(/\d+$/, '')
|
||||
const baseName = sanitizeFilename(fileName)
|
||||
let candidate = isFile ? baseName + '.md' : baseName
|
||||
let counter = 1
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.tiptap {
|
||||
padding: 12px 60px;
|
||||
// 预留5px给scrollbar
|
||||
padding: 12px 55px 12px 60px;
|
||||
outline: none;
|
||||
min-height: 120px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@ -97,6 +97,7 @@ const PopupContainer: React.FC<Props> = ({
|
||||
afterClose={onClose}
|
||||
afterOpenChange={handleAfterOpenChange}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
centered>
|
||||
<EditorContainer>
|
||||
<RichEditor
|
||||
@ -108,6 +109,7 @@ const PopupContainer: React.FC<Props> = ({
|
||||
onCommandsReady={handleCommandsReady}
|
||||
minHeight={300}
|
||||
maxHeight={500}
|
||||
isFullWidth={true}
|
||||
className="rich-edit-popup-editor"
|
||||
/>
|
||||
</EditorContainer>
|
||||
|
||||
@ -1615,9 +1615,12 @@
|
||||
"new_folder": "New Folder",
|
||||
"new_note": "Create a new note",
|
||||
"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_one_file_allowed": "Only one file can be uploaded",
|
||||
"open_folder": "Open an external folder",
|
||||
"rename": "Rename",
|
||||
"rename_changed": "Due to security policies, the filename has been changed from {{original}} to {{final}}",
|
||||
"save": "Save to Notes",
|
||||
"settings": {
|
||||
"data": {
|
||||
|
||||
@ -1615,9 +1615,12 @@
|
||||
"new_folder": "新しいフォルダーを作成する",
|
||||
"new_note": "新規ノート作成",
|
||||
"no_content_to_copy": "コピーするコンテンツはありません",
|
||||
"no_file_selected": "アップロードするファイルを選択してください",
|
||||
"only_markdown": "Markdown ファイルのみをアップロードできます",
|
||||
"only_one_file_allowed": "アップロードできるファイルは1つだけです",
|
||||
"open_folder": "外部フォルダーを開きます",
|
||||
"rename": "名前の変更",
|
||||
"rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました",
|
||||
"save": "メモに保存する",
|
||||
"settings": {
|
||||
"data": {
|
||||
|
||||
@ -1615,9 +1615,12 @@
|
||||
"new_folder": "Новая папка",
|
||||
"new_note": "Создать заметку",
|
||||
"no_content_to_copy": "Нет контента для копирования",
|
||||
"no_file_selected": "Пожалуйста, выберите файл для загрузки",
|
||||
"only_markdown": "Только Markdown",
|
||||
"only_one_file_allowed": "Можно загрузить только один файл",
|
||||
"open_folder": "Откройте внешнюю папку",
|
||||
"rename": "переименовать",
|
||||
"rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}",
|
||||
"save": "Сохранить в заметки",
|
||||
"settings": {
|
||||
"data": {
|
||||
|
||||
@ -1615,9 +1615,12 @@
|
||||
"new_folder": "新建文件夹",
|
||||
"new_note": "新建笔记",
|
||||
"no_content_to_copy": "没有内容可复制",
|
||||
"no_file_selected": "请选择要上传的文件",
|
||||
"only_markdown": "仅支持 Markdown 格式",
|
||||
"only_one_file_allowed": "只能上传一个文件",
|
||||
"open_folder": "打开外部文件夹",
|
||||
"rename": "重命名",
|
||||
"rename_changed": "由于安全策略,文件名已从 {{original}} 更改为 {{final}}",
|
||||
"save": "保存到笔记",
|
||||
"settings": {
|
||||
"data": {
|
||||
|
||||
@ -1615,9 +1615,12 @@
|
||||
"new_folder": "新建文件夾",
|
||||
"new_note": "新建筆記",
|
||||
"no_content_to_copy": "沒有內容可複制",
|
||||
"no_file_selected": "請選擇要上傳的文件",
|
||||
"only_markdown": "僅支援 Markdown 格式",
|
||||
"only_one_file_allowed": "只能上傳一個文件",
|
||||
"open_folder": "打開外部文件夾",
|
||||
"rename": "重命名",
|
||||
"rename_changed": "由於安全策略,文件名已從 {{original}} 更改為 {{final}}",
|
||||
"save": "儲存到筆記",
|
||||
"settings": {
|
||||
"data": {
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
} from '@renderer/services/NotesService'
|
||||
import { getNotesTree, isParentNode, updateNodeInTree } from '@renderer/services/NotesTreeService'
|
||||
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 { FileChangeEvent } from '@shared/config/types'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
@ -37,6 +37,7 @@ const NotesPage: FC = () => {
|
||||
const { showWorkspace } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
const activeFilePath = useAppSelector(selectActiveFilePath)
|
||||
const sortType = useAppSelector(selectSortType)
|
||||
const { settings, notesPath, updateNotesPath } = useNotesSettings()
|
||||
|
||||
// 混合策略:useLiveQuery用于笔记树,React Query用于文件内容
|
||||
@ -53,6 +54,8 @@ const NotesPage: FC = () => {
|
||||
const isEditorInitialized = useRef(false)
|
||||
const lastContentRef = useRef<string>('')
|
||||
const isInitialSortApplied = useRef(false)
|
||||
const isRenamingRef = useRef(false)
|
||||
const isCreatingNoteRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const updateCharCount = () => {
|
||||
@ -131,7 +134,7 @@ const NotesPage: FC = () => {
|
||||
async function applyInitialSort() {
|
||||
if (notesTree.length > 0 && !isInitialSortApplied.current) {
|
||||
try {
|
||||
await sortAllLevels('sort_a2z')
|
||||
await sortAllLevels(sortType)
|
||||
isInitialSortApplied.current = true
|
||||
} catch (error) {
|
||||
logger.error('Failed to apply initial sorting:', error as Error)
|
||||
@ -140,14 +143,21 @@ const NotesPage: FC = () => {
|
||||
}
|
||||
|
||||
applyInitialSort()
|
||||
}, [notesTree.length])
|
||||
}, [notesTree.length, sortType])
|
||||
|
||||
// 处理树同步时的状态管理
|
||||
useEffect(() => {
|
||||
if (notesTree.length === 0) return
|
||||
|
||||
// 如果有activeFilePath但找不到对应节点,清空选择
|
||||
if (activeFilePath && !activeNode) {
|
||||
// 但要排除正在同步树结构、重命名或创建笔记的情况,避免在这些操作中误清空
|
||||
if (
|
||||
activeFilePath &&
|
||||
!activeNode &&
|
||||
!isSyncingTreeRef.current &&
|
||||
!isRenamingRef.current &&
|
||||
!isCreatingNoteRef.current
|
||||
) {
|
||||
dispatch(setActiveFilePath(undefined))
|
||||
}
|
||||
}, [notesTree, activeFilePath, activeNode, dispatch])
|
||||
@ -191,7 +201,7 @@ const NotesPage: FC = () => {
|
||||
invalidateFileContent(filePath)
|
||||
}
|
||||
} else {
|
||||
await initWorkSpace(notesPath)
|
||||
await initWorkSpace(notesPath, sortType)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -210,7 +220,7 @@ const NotesPage: FC = () => {
|
||||
|
||||
// 重新同步数据库,useLiveQuery会自动响应数据库变化
|
||||
try {
|
||||
await initWorkSpace(notesPath)
|
||||
await initWorkSpace(notesPath, sortType)
|
||||
} catch (error) {
|
||||
logger.error('Failed to sync database:', error as Error)
|
||||
} finally {
|
||||
@ -264,7 +274,8 @@ const NotesPage: FC = () => {
|
||||
dispatch,
|
||||
currentContent,
|
||||
debouncedSave,
|
||||
saveCurrentNote
|
||||
saveCurrentNote,
|
||||
sortType
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@ -273,7 +284,7 @@ const NotesPage: FC = () => {
|
||||
// 标记编辑器已初始化
|
||||
isEditorInitialized.current = true
|
||||
}
|
||||
}, [currentContent])
|
||||
}, [currentContent, activeFilePath])
|
||||
|
||||
// 切换文件时重置编辑器初始化状态并兜底保存
|
||||
useEffect(() => {
|
||||
@ -319,17 +330,27 @@ const NotesPage: FC = () => {
|
||||
const handleCreateNote = useCallback(
|
||||
async (name: string) => {
|
||||
try {
|
||||
isCreatingNoteRef.current = true
|
||||
|
||||
const targetPath = getTargetFolderPath()
|
||||
if (!targetPath) {
|
||||
throw new Error('No folder path selected')
|
||||
}
|
||||
const newNote = await createNote(name, '', targetPath)
|
||||
dispatch(setActiveFilePath(newNote.externalPath))
|
||||
setSelectedFolderId(null)
|
||||
|
||||
await sortAllLevels(sortType)
|
||||
} catch (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)
|
||||
}
|
||||
} else if (node.type === 'folder') {
|
||||
// 设置选中的文件夹,同时清除活动文件
|
||||
setSelectedFolderId(node.id)
|
||||
// 清除活动文件状态,这样文件的高亮会被清除
|
||||
dispatch(setActiveFilePath(undefined))
|
||||
await handleToggleExpanded(node.id)
|
||||
}
|
||||
},
|
||||
@ -432,6 +450,7 @@ const NotesPage: FC = () => {
|
||||
(nodeToDelete.externalPath === activeFilePath || isParentNode(notesTree, nodeId, activeNode?.id || ''))
|
||||
|
||||
await deleteNode(nodeId)
|
||||
await sortAllLevels(sortType)
|
||||
|
||||
// 如果删除的是当前活动节点或其父节点,清空编辑器
|
||||
if (isActiveNodeOrParent) {
|
||||
@ -444,24 +463,47 @@ const NotesPage: FC = () => {
|
||||
logger.error('Failed to delete node:', error as Error)
|
||||
}
|
||||
},
|
||||
[activeFilePath, activeNode, notesTree, dispatch, findNodeById]
|
||||
[findNodeById, notesTree, activeFilePath, activeNode?.id, sortType, dispatch]
|
||||
)
|
||||
|
||||
// 重命名节点
|
||||
const handleRenameNode = useCallback(
|
||||
async (nodeId: string, newName: string) => {
|
||||
try {
|
||||
isRenamingRef.current = true
|
||||
|
||||
const tree = await getNotesTree()
|
||||
const node = findNodeById(tree, nodeId)
|
||||
|
||||
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) {
|
||||
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') => {
|
||||
try {
|
||||
await moveNode(sourceNodeId, targetNodeId, position)
|
||||
await sortAllLevels(sortType)
|
||||
} catch (error) {
|
||||
logger.error('Failed to move nodes:', error as Error)
|
||||
}
|
||||
},
|
||||
[]
|
||||
[sortType]
|
||||
)
|
||||
|
||||
// 处理节点排序
|
||||
const handleSortNodes = useCallback(async (sortType: NotesSortType) => {
|
||||
try {
|
||||
await sortAllLevels(sortType)
|
||||
} catch (error) {
|
||||
logger.error('Failed to sort notes:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}, [])
|
||||
const handleSortNodes = useCallback(
|
||||
async (newSortType: NotesSortType) => {
|
||||
try {
|
||||
// 更新Redux中的排序类型
|
||||
dispatch(setSortType(newSortType))
|
||||
await sortAllLevels(newSortType)
|
||||
} catch (error) {
|
||||
logger.error('Failed to sort notes:', error as Error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const getCurrentNoteContent = useCallback(() => {
|
||||
if (settings.defaultEditMode === 'source') {
|
||||
|
||||
@ -2,11 +2,14 @@ import { loggerService } from '@logger'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
import { useActiveNode } from '@renderer/hooks/useNotesQuery'
|
||||
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 { Dropdown, Input, MenuProps } from 'antd'
|
||||
import { Dropdown, Input, InputRef, MenuProps } from 'antd'
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@ -19,7 +22,7 @@ import {
|
||||
Star,
|
||||
StarOff
|
||||
} 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 styled from 'styled-components'
|
||||
|
||||
@ -57,8 +60,8 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const { bases } = useKnowledgeBases()
|
||||
const { activeNode } = useActiveNode(notesTree)
|
||||
const sortType = useAppSelector(selectSortType)
|
||||
const [editingNodeId, setEditingNodeId] = useState<string | null>(null)
|
||||
const [editingName, setEditingName] = useState('')
|
||||
const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null)
|
||||
const [dragOverNodeId, setDragOverNodeId] = useState<string | null>(null)
|
||||
const [dragPosition, setDragPosition] = useState<'before' | 'inside' | 'after'>('inside')
|
||||
@ -66,8 +69,56 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
const [isShowSearch, setIsShowSearch] = useState(false)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [isDragOverSidebar, setIsDragOverSidebar] = useState(false)
|
||||
const [sortType, setSortType] = useState<NotesSortType>('sort_a2z')
|
||||
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(() => {
|
||||
onCreateFolder(t('notes.untitled_folder'))
|
||||
@ -79,30 +130,18 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
|
||||
const handleSelectSortType = useCallback(
|
||||
(selectedSortType: NotesSortType) => {
|
||||
setSortType(selectedSortType)
|
||||
onSortNodes(selectedSortType)
|
||||
},
|
||||
[onSortNodes]
|
||||
)
|
||||
|
||||
const handleStartEdit = useCallback((node: NotesTreeNode) => {
|
||||
setEditingNodeId(node.id)
|
||||
setEditingName(node.name)
|
||||
}, [])
|
||||
|
||||
const handleFinishEdit = useCallback(() => {
|
||||
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 handleStartEdit = useCallback(
|
||||
(node: NotesTreeNode) => {
|
||||
setEditingNodeId(node.id)
|
||||
inPlaceEdit.startEdit(node.name)
|
||||
},
|
||||
[inPlaceEdit]
|
||||
)
|
||||
|
||||
const handleDeleteNode = useCallback(
|
||||
(node: NotesTreeNode) => {
|
||||
@ -306,8 +345,10 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
|
||||
const renderTreeNode = useCallback(
|
||||
(node: NotesTreeNode, depth: number = 0) => {
|
||||
const isActive = node.id === activeNode?.id || (node.type === 'folder' && node.id === selectedFolderId)
|
||||
const isEditing = editingNodeId === node.id
|
||||
const isActive = selectedFolderId
|
||||
? 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 isDragging = draggedNodeId === node.id
|
||||
const isDragOver = dragOverNodeId === node.id
|
||||
@ -328,6 +369,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
isDragInside={isDragInside}
|
||||
isDragAfter={isDragAfter}
|
||||
draggable={!isEditing}
|
||||
data-node-id={node.id}
|
||||
onDragStart={(e) => handleDragStart(e, node)}
|
||||
onDragOver={(e) => handleDragOver(e, node)}
|
||||
onDragLeave={handleDragLeave}
|
||||
@ -361,15 +403,13 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
|
||||
{isEditing ? (
|
||||
<EditInput
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
onPressEnter={handleFinishEdit}
|
||||
onBlur={handleFinishEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancelEdit()
|
||||
}
|
||||
}}
|
||||
ref={inPlaceEdit.inputRef as Ref<InputRef>}
|
||||
value={inPlaceEdit.editValue}
|
||||
onChange={inPlaceEdit.handleInputChange}
|
||||
onPressEnter={inPlaceEdit.saveEdit}
|
||||
onBlur={inPlaceEdit.saveEdit}
|
||||
onKeyDown={inPlaceEdit.handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
autoFocus
|
||||
size="small"
|
||||
/>
|
||||
@ -388,24 +428,27 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
)
|
||||
},
|
||||
[
|
||||
activeNode,
|
||||
selectedFolderId,
|
||||
activeNode?.id,
|
||||
editingNodeId,
|
||||
editingName,
|
||||
inPlaceEdit.isEditing,
|
||||
inPlaceEdit.inputRef,
|
||||
inPlaceEdit.editValue,
|
||||
inPlaceEdit.handleInputChange,
|
||||
inPlaceEdit.saveEdit,
|
||||
inPlaceEdit.handleKeyDown,
|
||||
draggedNodeId,
|
||||
dragOverNodeId,
|
||||
dragPosition,
|
||||
onSelectNode,
|
||||
onToggleExpanded,
|
||||
handleFinishEdit,
|
||||
handleCancelEdit,
|
||||
getMenuItems,
|
||||
handleDragLeave,
|
||||
handleDragEnd,
|
||||
t,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
handleDragEnd,
|
||||
getMenuItems,
|
||||
t
|
||||
onSelectNode,
|
||||
onToggleExpanded
|
||||
]
|
||||
)
|
||||
|
||||
@ -451,7 +494,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
/>
|
||||
|
||||
<NotesTreeContainer>
|
||||
<StyledScrollbar>
|
||||
<StyledScrollbar ref={scrollbarRef}>
|
||||
<TreeContent>
|
||||
{filteredTree.map((node) => renderTreeNode(node))}
|
||||
{!isShowStarred && !isShowSearch && (
|
||||
@ -480,6 +523,7 @@ const SidebarContainer = styled.div`
|
||||
height: 100vh;
|
||||
background-color: var(--color-background);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-top-left-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
@ -70,7 +70,7 @@ const NotesSettings: FC = () => {
|
||||
}
|
||||
|
||||
updateNotesPath(tempPath)
|
||||
initWorkSpace(tempPath)
|
||||
initWorkSpace(tempPath, 'sort_a2z')
|
||||
window.message.success(t('notes.settings.data.path_updated'))
|
||||
} catch (error) {
|
||||
logger.error('Failed to apply notes path:', error as Error)
|
||||
@ -83,7 +83,7 @@ const NotesSettings: FC = () => {
|
||||
const info = await window.api.getAppInfo()
|
||||
setTempPath(info.notesPath)
|
||||
updateNotesPath(info.notesPath)
|
||||
initWorkSpace(info.notesPath)
|
||||
initWorkSpace(info.notesPath, 'sort_a2z')
|
||||
window.message.success(t('notes.settings.data.reset_to_default'))
|
||||
} catch (error) {
|
||||
logger.error('Failed to reset to default:', error as Error)
|
||||
|
||||
@ -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)
|
||||
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 {
|
||||
const tree = await getNotesTree()
|
||||
if (!tree) {
|
||||
tree = await getNotesTree()
|
||||
}
|
||||
sortNodesArray(tree, sortType)
|
||||
recursiveSortNodes(tree, sortType)
|
||||
await db.notes_tree.put({ id: NOTES_TREE_ID, tree })
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { RootState } from '@renderer/store/index'
|
||||
import { EditorView } from '@renderer/types'
|
||||
import { NotesSortType } from '@renderer/types/note'
|
||||
|
||||
export interface NotesSettings {
|
||||
isFullWidth: boolean
|
||||
@ -15,6 +16,7 @@ export interface NoteState {
|
||||
activeFilePath: string | undefined // 使用文件路径而不是nodeId
|
||||
settings: NotesSettings
|
||||
notesPath: string
|
||||
sortType: NotesSortType
|
||||
}
|
||||
|
||||
export const initialState: NoteState = {
|
||||
@ -27,7 +29,8 @@ export const initialState: NoteState = {
|
||||
defaultEditMode: 'preview',
|
||||
showTabStatus: true
|
||||
},
|
||||
notesPath: ''
|
||||
notesPath: '',
|
||||
sortType: 'sort_a2z'
|
||||
}
|
||||
|
||||
const noteSlice = createSlice({
|
||||
@ -45,15 +48,19 @@ const noteSlice = createSlice({
|
||||
},
|
||||
setNotesPath: (state, action: PayloadAction<string>) => {
|
||||
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 selectActiveFilePath = (state: RootState) => state.note.activeFilePath
|
||||
export const selectNotesSettings = (state: RootState) => state.note.settings
|
||||
export const selectNotesPath = (state: RootState) => state.note.notesPath
|
||||
export const selectSortType = (state: RootState) => state.note.sortType
|
||||
|
||||
export default noteSlice.reducer
|
||||
|
||||
Loading…
Reference in New Issue
Block a user