diff --git a/src/App.tsx b/src/App.tsx
index 6dc6317a..1cbb8ed2 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -74,7 +74,7 @@ import { DeleteProgressNotice } from './components/DeleteProgressNotice'
import { UpdateBanner } from './components/UpdateBanner'
import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from './mock-tauri'
-import type { Settings, SidebarSelection, InboxPeriod, VaultEntry, ViewDefinition } from './types'
+import type { SidebarSelection, InboxPeriod, VaultEntry, ViewDefinition } from './types'
import type { NoteListItem } from './utils/ai-context'
import { initializeNoteProperties } from './utils/initializeNoteProperties'
import { filterEntries, filterInboxEntries, type NoteListFilter } from './utils/noteListHelpers'
@@ -1103,23 +1103,6 @@ function App() {
}
}, [refreshVaultAiGuidance, resolvedPath, vault, setToastMessage])
- const handleSaveSettings = useCallback((nextSettings: Settings) => {
- void (async () => {
- const saved = await saveSettings(nextSettings)
- if (!saved) return
-
- const guidancePreferenceChanged = (
- settings.default_resource_note_skill_enabled ?? false
- ) !== (
- nextSettings.default_resource_note_skill_enabled ?? false
- )
-
- if (guidancePreferenceChanged) {
- await restoreVaultAiGuidance('Tolaria AI guidance updated')
- }
- })()
- }, [restoreVaultAiGuidance, saveSettings, settings.default_resource_note_skill_enabled])
-
const activeDeletedFile = useMemo(() => {
const activeTabPath = notes.activeTabPath
if (!activeTabPath) return null
@@ -1240,9 +1223,9 @@ function App() {
? getNextVisibleInboxEntry(visibleNotesRef.current, path)
: null
- await entryActions.handleToggleOrganized(path)
+ const organized = await entryActions.handleToggleOrganized(path)
- if (nextVisibleInboxEntry) {
+ if (organized && nextVisibleInboxEntry) {
void notes.handleSelectNote(nextVisibleInboxEntry)
}
}, [effectiveSelection, entryActions, notes, settings.auto_advance_inbox_after_organize, vault.entries])
@@ -1550,7 +1533,7 @@ function App() {
onCommit={conflictResolver.commitResolution}
onClose={conflictFlow.handleCloseConflictResolver}
/>
-
+
diff --git a/src/hooks/useBulkActions.test.ts b/src/hooks/useBulkActions.test.ts
index 34b275b2..6b1eada4 100644
--- a/src/hooks/useBulkActions.test.ts
+++ b/src/hooks/useBulkActions.test.ts
@@ -16,7 +16,7 @@ describe('useBulkActions', () => {
beforeEach(() => {
handleArchiveNote = vi.fn().mockResolvedValue(undefined)
- handleToggleOrganized = vi.fn().mockResolvedValue(undefined)
+ handleToggleOrganized = vi.fn().mockResolvedValue(true)
setToastMessage = vi.fn()
})
@@ -116,7 +116,7 @@ describe('useBulkActions', () => {
it('skips failed organize actions and reports only successes', async () => {
handleToggleOrganized
- .mockResolvedValueOnce(undefined)
+ .mockResolvedValueOnce(true)
.mockRejectedValueOnce(new Error('fail'))
await runAction('handleBulkOrganize', [paths.a, paths.b])
@@ -124,5 +124,16 @@ describe('useBulkActions', () => {
expect(handleToggleOrganized).toHaveBeenCalledTimes(2)
expect(setToastMessage).toHaveBeenCalledWith('1 note organized')
})
+
+ it('does not count organize rollbacks as successes', async () => {
+ handleToggleOrganized
+ .mockResolvedValueOnce(false)
+ .mockResolvedValueOnce(true)
+
+ await runAction('handleBulkOrganize', [paths.a, paths.b])
+
+ expect(handleToggleOrganized).toHaveBeenCalledTimes(2)
+ expect(setToastMessage).toHaveBeenCalledWith('1 note organized')
+ })
})
})
diff --git a/src/hooks/useBulkActions.ts b/src/hooks/useBulkActions.ts
index 74befe0f..d91bc1df 100644
--- a/src/hooks/useBulkActions.ts
+++ b/src/hooks/useBulkActions.ts
@@ -3,7 +3,7 @@ import type { VaultEntry } from '../types'
interface BulkEntryActions {
handleArchiveNote: (path: string) => Promise
- handleToggleOrganized: (path: string) => Promise
+ handleToggleOrganized: (path: string) => Promise
}
function formatBulkToast(count: number, label: string) {
@@ -43,8 +43,7 @@ export function useBulkActions(
const handleBulkOrganize = useCallback(async (paths: string[]) => {
const ok = await runBulkAction(paths, async (path) => {
if (organizedPathSet.has(path)) return false
- await entryActions.handleToggleOrganized(path)
- return true
+ return entryActions.handleToggleOrganized(path)
})
if (ok > 0) setToastMessage(formatBulkToast(ok, 'organized'))
}, [entryActions, organizedPathSet, setToastMessage])
diff --git a/src/hooks/useEntryActions.test.ts b/src/hooks/useEntryActions.test.ts
index 209aa7fd..716626c4 100644
--- a/src/hooks/useEntryActions.test.ts
+++ b/src/hooks/useEntryActions.test.ts
@@ -481,6 +481,52 @@ describe('useEntryActions', () => {
})
})
+ describe('handleToggleOrganized', () => {
+ it('returns true after organizing is persisted', async () => {
+ const entry = makeEntry({ path: '/vault/note/test.md', organized: false })
+ const { result } = setup([entry])
+ let organized = false
+
+ await act(async () => {
+ organized = await result.current.handleToggleOrganized('/vault/note/test.md')
+ })
+
+ expect(organized).toBe(true)
+ expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', '_organized', true, { silent: true })
+ expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { organized: true })
+ })
+
+ it('returns false and rolls back when organizing fails', async () => {
+ const entry = makeEntry({ path: '/vault/note/test.md', organized: false })
+ handleUpdateFrontmatter.mockRejectedValueOnce(new Error('disk full'))
+ const { result } = setup([entry])
+ let organized = true
+
+ await act(async () => {
+ organized = await result.current.handleToggleOrganized('/vault/note/test.md')
+ })
+
+ expect(organized).toBe(false)
+ expect(updateEntry).toHaveBeenCalledTimes(2)
+ expect(updateEntry).toHaveBeenNthCalledWith(1, '/vault/note/test.md', { organized: true })
+ expect(updateEntry).toHaveBeenNthCalledWith(2, '/vault/note/test.md', { organized: false })
+ expect(setToastMessage).toHaveBeenCalledWith('Failed to organize — rolled back')
+ })
+
+ it('returns false when the entry is missing', async () => {
+ const { result } = setup([])
+ let organized = true
+
+ await act(async () => {
+ organized = await result.current.handleToggleOrganized('/vault/missing.md')
+ })
+
+ expect(organized).toBe(false)
+ expect(handleUpdateFrontmatter).not.toHaveBeenCalled()
+ expect(handleDeleteProperty).not.toHaveBeenCalled()
+ })
+ })
+
describe('handleReorderFavorites', () => {
it('updates _favorite_index for all reordered paths', async () => {
const { result } = setup()
diff --git a/src/hooks/useEntryActions.ts b/src/hooks/useEntryActions.ts
index 00dc3211..5c609bf1 100644
--- a/src/hooks/useEntryActions.ts
+++ b/src/hooks/useEntryActions.ts
@@ -126,16 +126,18 @@ export function useEntryActions({
const handleToggleOrganized = useCallback(async (path: string) => {
const entry = entries.find((e) => e.path === path)
- if (!entry) return
+ if (!entry) return false
if (entry.organized) {
trackEvent('note_unorganized')
updateEntry(path, { organized: false })
try {
await handleDeleteProperty(path, '_organized', { silent: true })
onFrontmatterPersisted?.()
+ return true
} catch {
updateEntry(path, { organized: true })
setToastMessage('Failed to unorganize — rolled back')
+ return false
}
} else {
trackEvent('note_organized')
@@ -143,9 +145,11 @@ export function useEntryActions({
try {
await handleUpdateFrontmatter(path, '_organized', true, { silent: true })
onFrontmatterPersisted?.()
+ return true
} catch {
updateEntry(path, { organized: false })
setToastMessage('Failed to organize — rolled back')
+ return false
}
}
}, [entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted])