fix: harden inbox auto-advance merge
This commit is contained in:
25
src/App.tsx
25
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}
|
||||
/>
|
||||
<SettingsPanel open={dialogs.showSettings} settings={settings} aiAgentsStatus={aiAgentsStatus} isGitVault={isGitVault} onSave={handleSaveSettings} explicitOrganizationEnabled={explicitOrganizationEnabled} onSaveExplicitOrganization={handleSaveExplicitOrganization} onClose={dialogs.closeSettings} />
|
||||
<SettingsPanel open={dialogs.showSettings} settings={settings} aiAgentsStatus={aiAgentsStatus} isGitVault={isGitVault} onSave={saveSettings} explicitOrganizationEnabled={explicitOrganizationEnabled} onSaveExplicitOrganization={handleSaveExplicitOrganization} onClose={dialogs.closeSettings} />
|
||||
<FeedbackDialog open={showFeedback} onClose={closeFeedback} />
|
||||
<McpSetupDialog open={showMcpSetupDialog} status={mcpStatus} busyAction={mcpDialogAction} onClose={closeMcpSetupDialog} onConnect={handleConnectMcp} onDisconnect={handleDisconnectMcp} />
|
||||
<CloneVaultModal key={dialogs.showCloneVault ? 'clone-open' : 'clone-closed'} open={dialogs.showCloneVault} onClose={dialogs.closeCloneVault} onVaultCloned={vaultSwitcher.handleVaultCloned} />
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { VaultEntry } from '../types'
|
||||
|
||||
interface BulkEntryActions {
|
||||
handleArchiveNote: (path: string) => Promise<void>
|
||||
handleToggleOrganized: (path: string) => Promise<void>
|
||||
handleToggleOrganized: (path: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user