diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 25547b62..04f4b90e 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -22,6 +22,98 @@ import { } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' +function useViewFlags(selection: SidebarSelection) { + const isSectionGroup = selection.kind === 'sectionGroup' + const isFolderView = selection.kind === 'folder' + const isInboxView = selection.kind === 'filter' && selection.filter === 'inbox' + const isAllNotesView = selection.kind === 'filter' && selection.filter === 'all' + const isChangesView = selection.kind === 'filter' && selection.filter === 'changes' + const showFilterPills = isSectionGroup || isFolderView || isAllNotesView + return { isSectionGroup, isFolderView, isInboxView, isAllNotesView, isChangesView, showFilterPills } +} + +function useBulkActions( + multiSelect: ReturnType, + onBulkArchive: NoteListProps['onBulkArchive'], + onBulkTrash: NoteListProps['onBulkTrash'], + onBulkRestore: NoteListProps['onBulkRestore'], + onBulkDeletePermanently: NoteListProps['onBulkDeletePermanently'], + isTrashView: boolean, + isArchivedView: boolean, +) { + const handleBulkArchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkArchive?.(paths) }, [multiSelect, onBulkArchive]) + const handleBulkTrash = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkTrash?.(paths) }, [multiSelect, onBulkTrash]) + const handleBulkRestore = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkRestore?.(paths) }, [multiSelect, onBulkRestore]) + const handleBulkDeletePermanently = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkDeletePermanently?.(paths) }, [multiSelect, onBulkDeletePermanently]) + const handleBulkUnarchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkRestore?.(paths) }, [multiSelect, onBulkRestore]) + const bulkArchiveOrRestore = isTrashView ? handleBulkRestore : isArchivedView ? handleBulkUnarchive : handleBulkArchive + const bulkTrashOrDelete = isTrashView ? handleBulkDeletePermanently : handleBulkTrash + return { handleBulkArchive, handleBulkTrash, handleBulkRestore, handleBulkDeletePermanently, handleBulkUnarchive, bulkArchiveOrRestore, bulkTrashOrDelete } +} + +function ChangesContextMenu({ isChangesView, onDiscardFile, modifiedFiles }: { isChangesView: boolean; onDiscardFile?: (relativePath: string) => Promise; modifiedFiles?: ModifiedFile[] }) { + const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; entry: VaultEntry } | null>(null) + const [discardTarget, setDiscardTarget] = useState(null) + const ctxMenuRef = useRef(null) + + const handleNoteContextMenu = useCallback((entry: VaultEntry, e: React.MouseEvent) => { + if (!isChangesView || !onDiscardFile) return + e.preventDefault() + e.stopPropagation() + setCtxMenu({ x: e.clientX, y: e.clientY, entry }) + }, [isChangesView, onDiscardFile]) + + const closeCtxMenu = useCallback(() => setCtxMenu(null), []) + + useEffect(() => { + if (!ctxMenu) return + const handler = (e: MouseEvent) => { + if (ctxMenuRef.current && !ctxMenuRef.current.contains(e.target as Node)) closeCtxMenu() + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, [ctxMenu, closeCtxMenu]) + + const handleDiscardConfirm = useCallback(async () => { + if (!discardTarget || !onDiscardFile) return + const mf = modifiedFiles?.find((f) => f.path === discardTarget.path) + if (!mf) return + await onDiscardFile(mf.relativePath) + setDiscardTarget(null) + }, [discardTarget, onDiscardFile, modifiedFiles]) + + const contextMenuNode = ctxMenu ? ( +
+ +
+ ) : null + + const dialogNode = ( + { if (!open) setDiscardTarget(null) }}> + + + Discard changes + + Discard changes to {discardTarget?.title ?? 'this file'}? This cannot be undone. + + + + + + + + + ) + + return { handleNoteContextMenu, contextMenuNode, dialogNode } +} + interface NoteListProps { entries: VaultEntry[] selection: SidebarSelection @@ -52,15 +144,11 @@ interface NoteListProps { function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNoteListFilterChange, inboxPeriod = 'all', modifiedFiles, modifiedFilesError, getNoteStatus, sidebarCollapsed, onSelectNote, onReplaceActiveTab, onCreateNote, onBulkArchive, onBulkTrash, onBulkRestore, onBulkDeletePermanently, onEmptyTrash, onUpdateTypeSort, updateEntry, onOpenInNewWindow, onDiscardFile, views }: NoteListProps) { const { modifiedPathSet, modifiedSuffixes, resolvedGetNoteStatus } = useModifiedFilesState(modifiedFiles, getNoteStatus) - const isSectionGroup = selection.kind === 'sectionGroup' - const isFolderView = selection.kind === 'folder' - const isInboxView = selection.kind === 'filter' && selection.filter === 'inbox' - const isAllNotesView = selection.kind === 'filter' && selection.filter === 'all' - const showFilterPills = isSectionGroup || isFolderView || isAllNotesView + const { isSectionGroup, isFolderView, isInboxView, isAllNotesView, isChangesView, showFilterPills } = useViewFlags(selection) const subFilter = showFilterPills ? noteListFilter : undefined const filterCounts = useMemo( - () => isSectionGroup ? countByFilter(entries, selection.type) : (isAllNotesView || isFolderView) ? countAllByFilter(entries) : { open: 0, archived: 0, trashed: 0 }, + () => isSectionGroup && selection.kind === 'sectionGroup' ? countByFilter(entries, selection.type) : (isAllNotesView || isFolderView) ? countAllByFilter(entries) : { open: 0, archived: 0, trashed: 0 }, [entries, isSectionGroup, isAllNotesView, isFolderView, selection], ) @@ -70,7 +158,6 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot const typeEntryMap = useTypeEntryMap(entries) const { isEntityView, isTrashView, isArchivedView, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes, subFilter, inboxPeriod: isInboxView ? inboxPeriod : undefined, views }) - const isChangesView = selection.kind === 'filter' && selection.filter === 'changes' const deletedCount = useMemo( () => isChangesView ? (modifiedFiles ?? []).filter((f) => f.status === 'deleted').length : 0, [isChangesView, modifiedFiles], @@ -85,46 +172,10 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot routeNoteClick(entry, e, { onReplace: onReplaceActiveTab, onSelect: onSelectNote, onOpenInNewWindow, multiSelect }) }, [onReplaceActiveTab, onSelectNote, onOpenInNewWindow, multiSelect]) - const handleBulkArchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkArchive?.(paths) }, [multiSelect, onBulkArchive]) - const handleBulkTrash = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkTrash?.(paths) }, [multiSelect, onBulkTrash]) - const handleBulkRestore = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkRestore?.(paths) }, [multiSelect, onBulkRestore]) - const handleBulkDeletePermanently = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkDeletePermanently?.(paths) }, [multiSelect, onBulkDeletePermanently]) - const handleBulkUnarchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkRestore?.(paths) }, [multiSelect, onBulkRestore]) - const bulkArchiveOrRestore = isTrashView ? handleBulkRestore : isArchivedView ? handleBulkUnarchive : handleBulkArchive - const bulkTrashOrDelete = isTrashView ? handleBulkDeletePermanently : handleBulkTrash + const { handleBulkArchive, handleBulkTrash, handleBulkRestore, handleBulkDeletePermanently, handleBulkUnarchive, bulkArchiveOrRestore, bulkTrashOrDelete } = useBulkActions(multiSelect, onBulkArchive, onBulkTrash, onBulkRestore, onBulkDeletePermanently, isTrashView, isArchivedView) useMultiSelectKeyboard(multiSelect, isEntityView, bulkArchiveOrRestore, bulkTrashOrDelete) - // ── Changes view: context menu + discard confirmation ── - const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; entry: VaultEntry } | null>(null) - const [discardTarget, setDiscardTarget] = useState(null) - const ctxMenuRef = useRef(null) - - const handleNoteContextMenu = useCallback((entry: VaultEntry, e: React.MouseEvent) => { - if (!isChangesView || !onDiscardFile) return - e.preventDefault() - e.stopPropagation() - setCtxMenu({ x: e.clientX, y: e.clientY, entry }) - }, [isChangesView, onDiscardFile]) - - const closeCtxMenu = useCallback(() => setCtxMenu(null), []) - - // Close context menu on outside click - useEffect(() => { - if (!ctxMenu) return - const handler = (e: MouseEvent) => { - if (ctxMenuRef.current && !ctxMenuRef.current.contains(e.target as Node)) closeCtxMenu() - } - document.addEventListener('mousedown', handler) - return () => document.removeEventListener('mousedown', handler) - }, [ctxMenu, closeCtxMenu]) - - const handleDiscardConfirm = useCallback(async () => { - if (!discardTarget || !onDiscardFile) return - const mf = modifiedFiles?.find((f) => f.path === discardTarget.path) - if (!mf) return - await onDiscardFile(mf.relativePath) - setDiscardTarget(null) - }, [discardTarget, onDiscardFile, modifiedFiles]) + const { handleNoteContextMenu, contextMenuNode, dialogNode } = ChangesContextMenu({ isChangesView, onDiscardFile, modifiedFiles }) const renderItem = useCallback((entry: VaultEntry) => ( @@ -153,35 +204,8 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot {multiSelect.isMultiSelecting && ( )} - - {/* Changes view: context menu */} - {ctxMenu && ( -
- -
- )} - - {/* Discard confirmation dialog */} - { if (!open) setDiscardTarget(null) }}> - - - Discard changes - - Discard changes to {discardTarget?.title ?? 'this file'}? This cannot be undone. - - - - - - - - + {contextMenuNode} + {dialogNode} ) } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b7d2622b..2ecfee11 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -587,7 +587,7 @@ export const Sidebar = memo(function Sidebar({ )} {/* Types */} - toggleGroup('sections')} showCustomize={showCustomize} setShowCustomize={setShowCustomize} isSectionVisible={isSectionVisible} toggleVisibility={toggleVisibility} onCreateNewType={onCreateNewType} customizeRef={customizeRef} /> + toggleGroup('sections')} showCustomize={showCustomize} setShowCustomize={setShowCustomize} isSectionVisible={isSectionVisible} toggleVisibility={toggleVisibility} onCreateNewType={onCreateNewType} customizeRef={customizeRef} /> {/* Folder tree */} toggleGroup('folders')} />