From 7b02dc1f9dc3780f5d5e7900f2a1344f598810b5 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 24 Feb 2026 14:42:06 +0100 Subject: [PATCH 1/2] feat: add Changes view for pending modifications Clicking "N pending" in the status bar or the "Changes" nav item in the sidebar shows a filtered list of notes with uncommitted changes. The view updates in real-time as files are saved or committed. - Add 'changes' filter to SidebarSelection type - Make StatusBar "N pending" indicator clickable - Add "Changes" NavItem in sidebar (visible when modifiedCount > 0) - Filter NoteList by modifiedFiles paths for changes view - Show "No pending changes" empty state Co-Authored-By: Claude Opus 4.6 --- src/App.tsx | 2 +- src/components/NoteList.tsx | 23 +++++++++++++++-------- src/components/Sidebar.test.tsx | 4 +++- src/components/Sidebar.tsx | 5 ++++- src/components/StatusBar.tsx | 13 +++++++++++-- src/types.ts | 2 +- 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8564f4f6..7b502aa8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -246,7 +246,7 @@ function App() { /> - setShowSettings(true)} onOpenLocalFolder={handleOpenLocalFolder} onConnectGitHub={() => setShowGitHubVault(true)} hasGitHub={!!settings.github_token} /> + setShowSettings(true)} onOpenLocalFolder={handleOpenLocalFolder} onConnectGitHub={() => setShowGitHubVault(true)} onClickPending={() => setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} /> setToastMessage(null)} /> setShowQuickOpen(false)} /> setShowCreateTypeDialog(false)} onCreate={handleCreateType} /> diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 2b3d4414..da80bbce 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -99,6 +99,7 @@ function resolveHeaderTitle(selection: SidebarSelection, typeDocument: VaultEntr if (typeDocument) return typeDocument.title if (selection.kind === 'filter' && selection.filter === 'archived') return 'Archive' if (selection.kind === 'filter' && selection.filter === 'trash') return 'Trash' + if (selection.kind === 'filter' && selection.filter === 'changes') return 'Changes' return 'Notes' } @@ -146,13 +147,13 @@ function ListViewHeader({ typeDocument, isTrashView, expiredTrashCount, typeEntr ) } -function ListView({ typeDocument, isTrashView, expiredTrashCount, searched, query, renderItem, typeEntryMap, onClickNote }: { - typeDocument: VaultEntry | null; isTrashView: boolean; expiredTrashCount: number +function ListView({ typeDocument, isTrashView, isChangesView, expiredTrashCount, searched, query, renderItem, typeEntryMap, onClickNote }: { + typeDocument: VaultEntry | null; isTrashView: boolean; isChangesView?: boolean; expiredTrashCount: number searched: VaultEntry[]; query: string renderItem: (entry: VaultEntry) => React.ReactNode typeEntryMap: Record; onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void }) { - const emptyText = isTrashView ? 'Trash is empty' : (query ? 'No matching notes' : 'No notes found') + const emptyText = isChangesView ? 'No pending changes' : isTrashView ? 'Trash is empty' : (query ? 'No matching notes' : 'No notes found') const hasHeader = typeDocument || (isTrashView && expiredTrashCount > 0) if (searched.length === 0) { @@ -208,11 +209,13 @@ function routeNoteClick( interface NoteListDataParams { entries: VaultEntry[]; selection: SidebarSelection; allContent: Record query: string; listSort: SortOption; listDirection: SortDirection + modifiedPathSet: Set } -function useNoteListData({ entries, selection, allContent, query, listSort, listDirection }: NoteListDataParams) { +function useNoteListData({ entries, selection, allContent, query, listSort, listDirection, modifiedPathSet }: NoteListDataParams) { const isEntityView = selection.kind === 'entity' const isTrashView = selection.kind === 'filter' && selection.filter === 'trash' + const isChangesView = selection.kind === 'filter' && selection.filter === 'changes' const typeDocument = useMemo(() => { if (selection.kind !== 'sectionGroup') return null @@ -221,9 +224,13 @@ function useNoteListData({ entries, selection, allContent, query, listSort, list const searched = useMemo(() => { if (isEntityView) return [] + if (isChangesView) { + const sorted = [...entries.filter((e) => modifiedPathSet.has(e.path))].sort(getSortComparator(listSort, listDirection)) + return filterByQuery(sorted, query) + } const sorted = [...filterEntries(entries, selection)].sort(getSortComparator(listSort, listDirection)) return filterByQuery(sorted, query) - }, [entries, selection, isEntityView, listSort, listDirection, query]) + }, [entries, selection, isEntityView, isChangesView, listSort, listDirection, query, modifiedPathSet]) const searchedGroups = useMemo(() => { if (!isEntityView) return [] @@ -236,7 +243,7 @@ function useNoteListData({ entries, selection, allContent, query, listSort, list [isTrashView, searched], ) - return { isEntityView, isTrashView, typeDocument, searched, searchedGroups, expiredTrashCount } + return { isEntityView, isTrashView, isChangesView, typeDocument, searched, searchedGroups, expiredTrashCount } } // --- Main component --- @@ -265,7 +272,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF const listConfig = sortPrefs['__list__'] ?? { option: 'modified' as SortOption, direction: 'desc' as SortDirection } const listSort = listConfig.option const listDirection = listConfig.direction - const { isEntityView, isTrashView, typeDocument, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, allContent, query, listSort, listDirection }) + const { isEntityView, isTrashView, isChangesView, typeDocument, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, allContent, query, listSort, listDirection, modifiedPathSet }) const handleClickNote = useCallback((entry: VaultEntry, e: React.MouseEvent) => { routeNoteClick(entry, e, onSelectNote, onReplaceActiveTab) @@ -300,7 +307,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF {isEntityView && selection.kind === 'entity' ? ( ) : ( - + )} diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index d74e56f6..50c7b9a8 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -334,7 +334,9 @@ describe('Sidebar', () => { it('shows badge on commit button when modified files exist', () => { render( {}} modifiedCount={3} onCommitPush={() => {}} />) - expect(screen.getByText('3')).toBeInTheDocument() + expect(screen.getByText('Commit & Push')).toBeInTheDocument() + const badges = screen.getAllByText('3') + expect(badges.length).toBeGreaterThanOrEqual(1) }) describe('dynamic custom type sections', () => { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d941fd58..7ebdc53c 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,7 +12,7 @@ import { import { CSS } from '@dnd-kit/utilities' import { FileText, Star, Wrench, Flask, Target, ArrowsClockwise, - Users, CalendarBlank, Tag, TagSimple, Trash, StackSimple, Archive, CaretLeft, + Users, CalendarBlank, Tag, TagSimple, Trash, StackSimple, Archive, CaretLeft, GitDiff, } from '@phosphor-icons/react' import { GitCommitHorizontal, SlidersHorizontal } from 'lucide-react' import { @@ -296,6 +296,9 @@ export const Sidebar = memo(function Sidebar({ onSelect({ kind: 'filter', filter: 'archived' })} /> onSelect({ kind: 'filter', filter: 'trash' })} /> + {modifiedCount > 0 && ( + onSelect({ kind: 'filter', filter: 'changes' })} /> + )} {/* Sections header + visibility popover */} diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index b2e5faae..0f94fd37 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -15,6 +15,7 @@ interface StatusBarProps { onOpenSettings?: () => void onOpenLocalFolder?: () => void onConnectGitHub?: () => void + onClickPending?: () => void hasGitHub?: boolean } @@ -104,7 +105,7 @@ const ICON_STYLE = { display: 'flex', alignItems: 'center', gap: 4 } as const const DISABLED_STYLE = { display: 'flex', alignItems: 'center', opacity: 0.4, cursor: 'not-allowed' } as const const SEP_STYLE = { color: 'var(--border)' } as const -export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onConnectGitHub, hasGitHub }: StatusBarProps) { +export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onConnectGitHub, onClickPending, hasGitHub }: StatusBarProps) { return (