feat: add inbox auto-advance setting
This commit is contained in:
@@ -11,6 +11,7 @@ pub struct Settings {
|
||||
pub autogit_enabled: Option<bool>,
|
||||
pub autogit_idle_threshold_seconds: Option<u32>,
|
||||
pub autogit_inactive_threshold_seconds: Option<u32>,
|
||||
pub auto_advance_inbox_after_organize: Option<bool>,
|
||||
pub telemetry_consent: Option<bool>,
|
||||
pub crash_reporting_enabled: Option<bool>,
|
||||
pub analytics_enabled: Option<bool>,
|
||||
@@ -62,6 +63,7 @@ fn normalize_settings(settings: Settings) -> Settings {
|
||||
autogit_inactive_threshold_seconds: normalize_optional_positive_u32(
|
||||
settings.autogit_inactive_threshold_seconds,
|
||||
),
|
||||
auto_advance_inbox_after_organize: settings.auto_advance_inbox_after_organize,
|
||||
telemetry_consent: settings.telemetry_consent,
|
||||
crash_reporting_enabled: settings.crash_reporting_enabled,
|
||||
analytics_enabled: settings.analytics_enabled,
|
||||
@@ -172,6 +174,7 @@ mod tests {
|
||||
Option<bool>,
|
||||
Option<bool>,
|
||||
Option<bool>,
|
||||
Option<bool>,
|
||||
Option<&'a str>,
|
||||
Option<&'a str>,
|
||||
Option<bool>,
|
||||
@@ -184,6 +187,7 @@ mod tests {
|
||||
settings.autogit_enabled,
|
||||
settings.autogit_idle_threshold_seconds,
|
||||
settings.autogit_inactive_threshold_seconds,
|
||||
settings.auto_advance_inbox_after_organize,
|
||||
settings.telemetry_consent,
|
||||
settings.crash_reporting_enabled,
|
||||
settings.analytics_enabled,
|
||||
@@ -197,7 +201,7 @@ mod tests {
|
||||
fn assert_empty_settings(settings: &Settings) {
|
||||
assert_eq!(
|
||||
settings_snapshot(settings),
|
||||
(None, None, None, None, None, None, None, None, None, None, None)
|
||||
(None, None, None, None, None, None, None, None, None, None, None, None)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,6 +238,7 @@ mod tests {
|
||||
autogit_enabled: Some(true),
|
||||
autogit_idle_threshold_seconds: Some(90),
|
||||
autogit_inactive_threshold_seconds: Some(30),
|
||||
auto_advance_inbox_after_organize: Some(true),
|
||||
telemetry_consent: Some(true),
|
||||
crash_reporting_enabled: Some(true),
|
||||
analytics_enabled: Some(false),
|
||||
@@ -262,6 +267,7 @@ mod tests {
|
||||
autogit_enabled: Some(true),
|
||||
autogit_idle_threshold_seconds: Some(90),
|
||||
autogit_inactive_threshold_seconds: Some(30),
|
||||
auto_advance_inbox_after_organize: Some(true),
|
||||
release_channel: Some("alpha".to_string()),
|
||||
initial_h1_auto_rename_enabled: Some(false),
|
||||
default_ai_agent: Some("codex".to_string()),
|
||||
@@ -271,6 +277,7 @@ mod tests {
|
||||
assert_eq!(loaded.autogit_enabled, Some(true));
|
||||
assert_eq!(loaded.autogit_idle_threshold_seconds, Some(90));
|
||||
assert_eq!(loaded.autogit_inactive_threshold_seconds, Some(30));
|
||||
assert_eq!(loaded.auto_advance_inbox_after_organize, Some(true));
|
||||
assert_eq!(loaded.release_channel.as_deref(), Some("alpha"));
|
||||
assert_eq!(loaded.initial_h1_auto_rename_enabled, Some(false));
|
||||
assert_eq!(loaded.default_ai_agent.as_deref(), Some("codex"));
|
||||
@@ -383,6 +390,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(true),
|
||||
Some(true),
|
||||
Some(false),
|
||||
|
||||
@@ -241,6 +241,7 @@ function resetMockCommandResults() {
|
||||
get_file_history: [],
|
||||
get_settings: {
|
||||
auto_pull_interval_minutes: null,
|
||||
auto_advance_inbox_after_organize: null,
|
||||
telemetry_consent: true,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
@@ -267,6 +268,7 @@ vi.mock('./mock-tauri', () => ({
|
||||
mockInvoke: vi.fn(async (cmd: string, args?: unknown) => resolveMockCommandResult(cmd, args)),
|
||||
addMockEntry: vi.fn(),
|
||||
updateMockContent: vi.fn(),
|
||||
trackMockChange: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock ai-chat utilities
|
||||
@@ -740,6 +742,44 @@ describe('App', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-advances to the next inbox item after organizing when the setting is enabled', async () => {
|
||||
configureNeighborhoodVault()
|
||||
mockCommandResults.get_settings = {
|
||||
auto_pull_interval_minutes: null,
|
||||
auto_advance_inbox_after_organize: true,
|
||||
telemetry_consent: true,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
anonymous_id: null,
|
||||
release_channel: null,
|
||||
}
|
||||
|
||||
render(<App />)
|
||||
|
||||
const noteListContainer = await screen.findByTestId('note-list-container')
|
||||
await waitFor(() => {
|
||||
expect(getHeaderForNoteList(noteListContainer)).toHaveTextContent('Inbox')
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(noteListContainer).getByText('Alpha'))
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Set note as organized' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Set note as organized' }))
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.__laputaTest?.activeTabPath).toBe('/vault/beta.md')
|
||||
})
|
||||
})
|
||||
|
||||
it('renders status bar', async () => {
|
||||
render(<App />)
|
||||
// StatusBar should be present
|
||||
|
||||
32
src/App.tsx
32
src/App.tsx
@@ -120,6 +120,12 @@ declare global {
|
||||
|
||||
const DEFAULT_SELECTION: SidebarSelection = INBOX_SELECTION
|
||||
|
||||
function getNextVisibleInboxEntry(entries: VaultEntry[], currentPath: string): VaultEntry | null {
|
||||
const currentIndex = entries.findIndex((entry) => entry.path === currentPath)
|
||||
if (currentIndex < 0) return null
|
||||
return entries[currentIndex + 1] ?? null
|
||||
}
|
||||
|
||||
function shouldPreferOnboardingVaultPath(
|
||||
onboardingState: { status: string; vaultPath?: string },
|
||||
vaults: Array<{ path: string }>,
|
||||
@@ -875,6 +881,7 @@ function App() {
|
||||
useEffect(() => {
|
||||
window.__laputaTest = {
|
||||
...window.__laputaTest,
|
||||
activeTabPath: notes.activeTabPath,
|
||||
seedAutoGitSavedChange,
|
||||
}
|
||||
|
||||
@@ -883,7 +890,7 @@ function App() {
|
||||
delete window.__laputaTest.seedAutoGitSavedChange
|
||||
}
|
||||
}
|
||||
}, [seedAutoGitSavedChange])
|
||||
}, [notes.activeTabPath, seedAutoGitSavedChange])
|
||||
|
||||
const entryActions = useEntryActions({
|
||||
entries: vault.entries, updateEntry: vault.updateEntry,
|
||||
@@ -1203,7 +1210,26 @@ function App() {
|
||||
const entry = vault.entries.find((candidate) => candidate.path === notes.activeTabPath)
|
||||
return hasNoteIconValue(entry?.icon)
|
||||
}, [notes.activeTabPath, vault.entries])
|
||||
const toggleOrganizedCommand = explicitOrganizationEnabled ? entryActions.handleToggleOrganized : undefined
|
||||
const handleToggleOrganizedWithInboxAdvance = useCallback(async (path: string) => {
|
||||
const entry = vault.entries.find((candidate) => candidate.path === path)
|
||||
if (!entry) return
|
||||
|
||||
const shouldAutoAdvance = settings.auto_advance_inbox_after_organize === true
|
||||
&& !entry.organized
|
||||
&& notes.activeTabPath === path
|
||||
&& effectiveSelection.kind === 'filter'
|
||||
&& effectiveSelection.filter === 'inbox'
|
||||
const nextVisibleInboxEntry = shouldAutoAdvance
|
||||
? getNextVisibleInboxEntry(visibleNotesRef.current, path)
|
||||
: null
|
||||
|
||||
await entryActions.handleToggleOrganized(path)
|
||||
|
||||
if (nextVisibleInboxEntry) {
|
||||
void notes.handleSelectNote(nextVisibleInboxEntry)
|
||||
}
|
||||
}, [effectiveSelection, entryActions, notes, settings.auto_advance_inbox_after_organize, vault.entries])
|
||||
const toggleOrganizedCommand = explicitOrganizationEnabled ? handleToggleOrganizedWithInboxAdvance : undefined
|
||||
const canCustomizeNoteListColumns = useMemo(() => (
|
||||
effectiveSelection.kind === 'view'
|
||||
|| (
|
||||
@@ -1437,7 +1463,7 @@ function App() {
|
||||
noteList={aiNoteList}
|
||||
noteListFilter={aiNoteListFilter}
|
||||
onToggleFavorite={activeDeletedFile ? undefined : entryActions.handleToggleFavorite}
|
||||
onToggleOrganized={activeDeletedFile || !explicitOrganizationEnabled ? undefined : entryActions.handleToggleOrganized}
|
||||
onToggleOrganized={activeDeletedFile || !explicitOrganizationEnabled ? undefined : toggleOrganizedCommand}
|
||||
onDeleteNote={activeDeletedFile ? undefined : deleteActions.handleDeleteNote}
|
||||
onArchiveNote={activeDeletedFile ? undefined : entryActions.handleArchiveNote}
|
||||
onUnarchiveNote={activeDeletedFile ? undefined : entryActions.handleUnarchiveNote}
|
||||
|
||||
@@ -8,6 +8,7 @@ const emptySettings: Settings = {
|
||||
autogit_enabled: null,
|
||||
autogit_idle_threshold_seconds: null,
|
||||
autogit_inactive_threshold_seconds: null,
|
||||
auto_advance_inbox_after_organize: null,
|
||||
telemetry_consent: null,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
@@ -138,6 +139,13 @@ describe('SettingsPanel', () => {
|
||||
expect(screen.getByRole('switch', { name: 'Organize notes explicitly' })).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('defaults auto-advance to the next inbox item to off', () => {
|
||||
render(
|
||||
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
||||
)
|
||||
expect(screen.getByRole('switch', { name: 'Auto-advance to next Inbox item' })).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('defaults the initial H1 auto-rename switch to on', () => {
|
||||
render(
|
||||
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
||||
@@ -220,6 +228,19 @@ describe('SettingsPanel', () => {
|
||||
expect(onSaveExplicitOrganization).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('saves the auto-advance inbox preference when toggled on', () => {
|
||||
render(
|
||||
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Auto-advance to next Inbox item' }))
|
||||
fireEvent.click(screen.getByTestId('settings-save'))
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
||||
auto_advance_inbox_after_organize: true,
|
||||
}))
|
||||
})
|
||||
|
||||
it('calls onClose when Cancel is clicked', () => {
|
||||
render(
|
||||
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
||||
|
||||
@@ -47,6 +47,7 @@ interface SettingsDraft {
|
||||
autoGitEnabled: boolean
|
||||
autoGitIdleThresholdSeconds: number
|
||||
autoGitInactiveThresholdSeconds: number
|
||||
autoAdvanceInboxAfterOrganize: boolean
|
||||
defaultAiAgent: AiAgentId
|
||||
releaseChannel: ReleaseChannel
|
||||
initialH1AutoRename: boolean
|
||||
@@ -65,6 +66,8 @@ interface SettingsBodyProps {
|
||||
setAutoGitIdleThresholdSeconds: (value: number) => void
|
||||
autoGitInactiveThresholdSeconds: number
|
||||
setAutoGitInactiveThresholdSeconds: (value: number) => void
|
||||
autoAdvanceInboxAfterOrganize: boolean
|
||||
setAutoAdvanceInboxAfterOrganize: (value: boolean) => void
|
||||
aiAgentsStatus: AiAgentsStatus
|
||||
defaultAiAgent: AiAgentId
|
||||
setDefaultAiAgent: (value: AiAgentId) => void
|
||||
@@ -103,6 +106,7 @@ function createSettingsDraft(
|
||||
settings.autogit_inactive_threshold_seconds,
|
||||
DEFAULT_AUTOGIT_INACTIVE_THRESHOLD_SECONDS,
|
||||
),
|
||||
autoAdvanceInboxAfterOrganize: settings.auto_advance_inbox_after_organize ?? false,
|
||||
defaultAiAgent: resolveDefaultAiAgent(settings.default_ai_agent),
|
||||
releaseChannel: normalizeReleaseChannel(settings.release_channel),
|
||||
initialH1AutoRename: settings.initial_h1_auto_rename_enabled ?? true,
|
||||
@@ -131,6 +135,7 @@ function buildSettingsFromDraft(settings: Settings, draft: SettingsDraft): Setti
|
||||
autogit_enabled: draft.autoGitEnabled,
|
||||
autogit_idle_threshold_seconds: draft.autoGitIdleThresholdSeconds,
|
||||
autogit_inactive_threshold_seconds: draft.autoGitInactiveThresholdSeconds,
|
||||
auto_advance_inbox_after_organize: draft.autoAdvanceInboxAfterOrganize,
|
||||
telemetry_consent: resolveTelemetryConsent(settings, draft),
|
||||
crash_reporting_enabled: draft.crashReporting,
|
||||
analytics_enabled: draft.analytics,
|
||||
@@ -267,6 +272,8 @@ function SettingsPanelInner({
|
||||
setAutoGitIdleThresholdSeconds={(value) => updateDraft('autoGitIdleThresholdSeconds', value)}
|
||||
autoGitInactiveThresholdSeconds={draft.autoGitInactiveThresholdSeconds}
|
||||
setAutoGitInactiveThresholdSeconds={(value) => updateDraft('autoGitInactiveThresholdSeconds', value)}
|
||||
autoAdvanceInboxAfterOrganize={draft.autoAdvanceInboxAfterOrganize}
|
||||
setAutoAdvanceInboxAfterOrganize={(value) => updateDraft('autoAdvanceInboxAfterOrganize', value)}
|
||||
aiAgentsStatus={aiAgentsStatus}
|
||||
defaultAiAgent={draft.defaultAiAgent}
|
||||
setDefaultAiAgent={(value) => updateDraft('defaultAiAgent', value)}
|
||||
@@ -317,6 +324,8 @@ function SettingsBody({
|
||||
setAutoGitIdleThresholdSeconds,
|
||||
autoGitInactiveThresholdSeconds,
|
||||
setAutoGitInactiveThresholdSeconds,
|
||||
autoAdvanceInboxAfterOrganize,
|
||||
setAutoAdvanceInboxAfterOrganize,
|
||||
aiAgentsStatus,
|
||||
defaultAiAgent,
|
||||
setDefaultAiAgent,
|
||||
@@ -373,6 +382,8 @@ function SettingsBody({
|
||||
<OrganizationWorkflowSection
|
||||
checked={explicitOrganization}
|
||||
onChange={setExplicitOrganization}
|
||||
autoAdvanceInboxAfterOrganize={autoAdvanceInboxAfterOrganize}
|
||||
onChangeAutoAdvanceInboxAfterOrganize={setAutoAdvanceInboxAfterOrganize}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -707,15 +718,19 @@ function LabeledNumberInput({
|
||||
function OrganizationWorkflowSection({
|
||||
checked,
|
||||
onChange,
|
||||
autoAdvanceInboxAfterOrganize,
|
||||
onChangeAutoAdvanceInboxAfterOrganize,
|
||||
}: {
|
||||
checked: boolean
|
||||
onChange: (value: boolean) => void
|
||||
autoAdvanceInboxAfterOrganize: boolean
|
||||
onChangeAutoAdvanceInboxAfterOrganize: (value: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<SectionHeading
|
||||
title="Workflow"
|
||||
description="Choose whether Tolaria shows the Inbox workflow and the organized toggle."
|
||||
description="Choose whether Tolaria shows the Inbox workflow, plus how it moves through items while you triage them."
|
||||
/>
|
||||
|
||||
<SettingsSwitchRow
|
||||
@@ -725,6 +740,14 @@ function OrganizationWorkflowSection({
|
||||
onChange={onChange}
|
||||
testId="settings-explicit-organization"
|
||||
/>
|
||||
|
||||
<SettingsSwitchRow
|
||||
label="Auto-advance to next Inbox item"
|
||||
description="When enabled, marking an Inbox note as organized immediately opens the next visible Inbox note."
|
||||
checked={autoAdvanceInboxAfterOrganize}
|
||||
onChange={onChangeAutoAdvanceInboxAfterOrganize}
|
||||
testId="settings-auto-advance-inbox-after-organize"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const defaultSettings: Settings = {
|
||||
autogit_enabled: null,
|
||||
autogit_idle_threshold_seconds: null,
|
||||
autogit_inactive_threshold_seconds: null,
|
||||
auto_advance_inbox_after_organize: null,
|
||||
telemetry_consent: null,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
@@ -21,6 +22,7 @@ const savedSettings: Settings = {
|
||||
autogit_enabled: true,
|
||||
autogit_idle_threshold_seconds: 90,
|
||||
autogit_inactive_threshold_seconds: 30,
|
||||
auto_advance_inbox_after_organize: true,
|
||||
telemetry_consent: null,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
@@ -103,6 +105,7 @@ describe('useSettings', () => {
|
||||
autogit_enabled: false,
|
||||
autogit_idle_threshold_seconds: 120,
|
||||
autogit_inactive_threshold_seconds: 45,
|
||||
auto_advance_inbox_after_organize: false,
|
||||
telemetry_consent: null,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
|
||||
@@ -14,6 +14,7 @@ const EMPTY_SETTINGS: Settings = {
|
||||
autogit_enabled: null,
|
||||
autogit_idle_threshold_seconds: null,
|
||||
autogit_inactive_threshold_seconds: null,
|
||||
auto_advance_inbox_after_organize: null,
|
||||
telemetry_consent: null,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
|
||||
@@ -146,6 +146,7 @@ describe('mockHandlers coverage', () => {
|
||||
autogit_enabled: true,
|
||||
autogit_idle_threshold_seconds: undefined,
|
||||
autogit_inactive_threshold_seconds: undefined,
|
||||
auto_advance_inbox_after_organize: true,
|
||||
telemetry_consent: true,
|
||||
crash_reporting_enabled: false,
|
||||
analytics_enabled: true,
|
||||
@@ -160,6 +161,7 @@ describe('mockHandlers coverage', () => {
|
||||
autogit_enabled: true,
|
||||
autogit_idle_threshold_seconds: 90,
|
||||
autogit_inactive_threshold_seconds: 30,
|
||||
auto_advance_inbox_after_organize: true,
|
||||
telemetry_consent: true,
|
||||
crash_reporting_enabled: false,
|
||||
analytics_enabled: true,
|
||||
|
||||
@@ -105,6 +105,7 @@ let mockSettings: Settings = {
|
||||
autogit_enabled: false,
|
||||
autogit_idle_threshold_seconds: 90,
|
||||
autogit_inactive_threshold_seconds: 30,
|
||||
auto_advance_inbox_after_organize: false,
|
||||
telemetry_consent: false,
|
||||
crash_reporting_enabled: null,
|
||||
analytics_enabled: null,
|
||||
@@ -409,6 +410,7 @@ export const mockHandlers: Record<string, (args: any) => any> = {
|
||||
autogit_enabled: s.autogit_enabled ?? false,
|
||||
autogit_idle_threshold_seconds: s.autogit_idle_threshold_seconds ?? 90,
|
||||
autogit_inactive_threshold_seconds: s.autogit_inactive_threshold_seconds ?? 30,
|
||||
auto_advance_inbox_after_organize: s.auto_advance_inbox_after_organize ?? false,
|
||||
telemetry_consent: s.telemetry_consent,
|
||||
crash_reporting_enabled: s.crash_reporting_enabled,
|
||||
analytics_enabled: s.analytics_enabled,
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface Settings {
|
||||
autogit_enabled?: boolean | null
|
||||
autogit_idle_threshold_seconds?: number | null
|
||||
autogit_inactive_threshold_seconds?: number | null
|
||||
auto_advance_inbox_after_organize?: boolean | null
|
||||
telemetry_consent: boolean | null
|
||||
crash_reporting_enabled: boolean | null
|
||||
analytics_enabled: boolean | null
|
||||
|
||||
Reference in New Issue
Block a user