feat: add h1 auto-rename setting
This commit is contained in:
@@ -13,6 +13,7 @@ pub struct Settings {
|
||||
pub analytics_enabled: Option<bool>,
|
||||
pub anonymous_id: Option<String>,
|
||||
pub release_channel: Option<String>,
|
||||
pub initial_h1_auto_rename_enabled: Option<bool>,
|
||||
pub default_ai_agent: Option<String>,
|
||||
}
|
||||
|
||||
@@ -52,6 +53,7 @@ fn normalize_settings(settings: Settings) -> Settings {
|
||||
analytics_enabled: settings.analytics_enabled,
|
||||
anonymous_id: normalize_optional_string(settings.anonymous_id),
|
||||
release_channel: normalize_release_channel(settings.release_channel.as_deref()),
|
||||
initial_h1_auto_rename_enabled: settings.initial_h1_auto_rename_enabled,
|
||||
default_ai_agent: normalize_default_ai_agent(settings.default_ai_agent.as_deref()),
|
||||
}
|
||||
}
|
||||
@@ -155,6 +157,7 @@ mod tests {
|
||||
Option<bool>,
|
||||
Option<&'a str>,
|
||||
Option<&'a str>,
|
||||
Option<bool>,
|
||||
Option<&'a str>,
|
||||
);
|
||||
|
||||
@@ -166,6 +169,7 @@ mod tests {
|
||||
settings.analytics_enabled,
|
||||
settings.anonymous_id.as_deref(),
|
||||
settings.release_channel.as_deref(),
|
||||
settings.initial_h1_auto_rename_enabled,
|
||||
settings.default_ai_agent.as_deref(),
|
||||
)
|
||||
}
|
||||
@@ -173,7 +177,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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,6 +216,7 @@ mod tests {
|
||||
analytics_enabled: Some(false),
|
||||
anonymous_id: Some("abc-123-uuid".to_string()),
|
||||
release_channel: Some("alpha".to_string()),
|
||||
initial_h1_auto_rename_enabled: Some(false),
|
||||
default_ai_agent: Some("codex".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&settings).unwrap();
|
||||
@@ -232,11 +237,13 @@ mod tests {
|
||||
let loaded = save_and_reload(Settings {
|
||||
auto_pull_interval_minutes: Some(10),
|
||||
release_channel: Some("alpha".to_string()),
|
||||
initial_h1_auto_rename_enabled: Some(false),
|
||||
default_ai_agent: Some("codex".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(loaded.auto_pull_interval_minutes, Some(10));
|
||||
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"));
|
||||
}
|
||||
|
||||
@@ -339,6 +346,7 @@ mod tests {
|
||||
Some("test-uuid-v4"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -480,6 +480,7 @@ function App() {
|
||||
tabs: notes.tabs, activeTabPath: notes.activeTabPath,
|
||||
handleRenameNote: notes.handleRenameNote, handleRenameFilename: notes.handleRenameFilename,
|
||||
replaceEntry: vault.replaceEntry, resolvedPath,
|
||||
initialH1AutoRenameEnabled: settings.initial_h1_auto_rename_enabled !== false,
|
||||
})
|
||||
|
||||
const aiActivity = useAiActivity({
|
||||
|
||||
@@ -109,6 +109,26 @@ describe('SettingsPanel', () => {
|
||||
expect(screen.getByRole('switch', { name: 'Organize notes explicitly' })).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('defaults the initial H1 auto-rename switch to on', () => {
|
||||
render(
|
||||
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
||||
)
|
||||
expect(screen.getByRole('switch', { name: 'Auto-rename untitled notes from first H1' })).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('saves the initial H1 auto-rename preference when toggled off', () => {
|
||||
render(
|
||||
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} />
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Auto-rename untitled notes from first H1' }))
|
||||
fireEvent.click(screen.getByTestId('settings-save'))
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
||||
initial_h1_auto_rename_enabled: false,
|
||||
}))
|
||||
})
|
||||
|
||||
it('saves the organization workflow preference when toggled off', () => {
|
||||
const onSaveExplicitOrganization = vi.fn()
|
||||
render(
|
||||
|
||||
@@ -43,6 +43,7 @@ interface SettingsDraft {
|
||||
pullInterval: number
|
||||
defaultAiAgent: AiAgentId
|
||||
releaseChannel: ReleaseChannel
|
||||
initialH1AutoRename: boolean
|
||||
crashReporting: boolean
|
||||
analytics: boolean
|
||||
explicitOrganization: boolean
|
||||
@@ -56,6 +57,8 @@ interface SettingsBodyProps {
|
||||
setDefaultAiAgent: (value: AiAgentId) => void
|
||||
releaseChannel: ReleaseChannel
|
||||
setReleaseChannel: (value: ReleaseChannel) => void
|
||||
initialH1AutoRename: boolean
|
||||
setInitialH1AutoRename: (value: boolean) => void
|
||||
explicitOrganization: boolean
|
||||
setExplicitOrganization: (value: boolean) => void
|
||||
crashReporting: boolean
|
||||
@@ -78,6 +81,7 @@ function createSettingsDraft(
|
||||
pullInterval: settings.auto_pull_interval_minutes ?? 5,
|
||||
defaultAiAgent: resolveDefaultAiAgent(settings.default_ai_agent),
|
||||
releaseChannel: normalizeReleaseChannel(settings.release_channel),
|
||||
initialH1AutoRename: settings.initial_h1_auto_rename_enabled ?? true,
|
||||
crashReporting: settings.crash_reporting_enabled ?? false,
|
||||
analytics: settings.analytics_enabled ?? false,
|
||||
explicitOrganization: explicitOrganizationEnabled,
|
||||
@@ -105,6 +109,7 @@ function buildSettingsFromDraft(settings: Settings, draft: SettingsDraft): Setti
|
||||
analytics_enabled: draft.analytics,
|
||||
anonymous_id: resolveAnonymousId(settings, draft),
|
||||
release_channel: serializeReleaseChannel(draft.releaseChannel),
|
||||
initial_h1_auto_rename_enabled: draft.initialH1AutoRename,
|
||||
default_ai_agent: draft.defaultAiAgent,
|
||||
}
|
||||
}
|
||||
@@ -224,6 +229,8 @@ function SettingsPanelInner({
|
||||
setDefaultAiAgent={(value) => updateDraft('defaultAiAgent', value)}
|
||||
releaseChannel={draft.releaseChannel}
|
||||
setReleaseChannel={(value) => updateDraft('releaseChannel', value)}
|
||||
initialH1AutoRename={draft.initialH1AutoRename}
|
||||
setInitialH1AutoRename={(value) => updateDraft('initialH1AutoRename', value)}
|
||||
explicitOrganization={draft.explicitOrganization}
|
||||
setExplicitOrganization={(value) => updateDraft('explicitOrganization', value)}
|
||||
crashReporting={draft.crashReporting}
|
||||
@@ -265,6 +272,8 @@ function SettingsBody({
|
||||
setDefaultAiAgent,
|
||||
releaseChannel,
|
||||
setReleaseChannel,
|
||||
initialH1AutoRename,
|
||||
setInitialH1AutoRename,
|
||||
explicitOrganization,
|
||||
setExplicitOrganization,
|
||||
crashReporting,
|
||||
@@ -293,6 +302,21 @@ function SettingsBody({
|
||||
|
||||
<Divider />
|
||||
|
||||
<SectionHeading
|
||||
title="Titles & Filenames"
|
||||
description="Choose whether Tolaria automatically syncs untitled note filenames from the first H1 title."
|
||||
/>
|
||||
|
||||
<SettingsSwitchRow
|
||||
label="Auto-rename untitled notes from first H1"
|
||||
description="When enabled, Tolaria renames untitled-note files as soon as the first H1 becomes a real title. Turn this off to keep the filename unchanged until you rename it manually from the breadcrumb bar."
|
||||
checked={initialH1AutoRename}
|
||||
onChange={setInitialH1AutoRename}
|
||||
testId="settings-initial-h1-auto-rename"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<SectionHeading
|
||||
title="AI Agents"
|
||||
description="Choose which CLI AI agent Tolaria uses in the AI panel and command palette."
|
||||
@@ -452,23 +476,45 @@ function OrganizationWorkflowSection({
|
||||
description="Choose whether Tolaria shows the Inbox workflow and the organized toggle."
|
||||
/>
|
||||
|
||||
<label
|
||||
className="flex items-start justify-between gap-3"
|
||||
style={{ cursor: 'pointer' }}
|
||||
data-testid="settings-explicit-organization"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--foreground)' }}>Organize notes explicitly</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>
|
||||
When enabled, an Inbox section shows unorganized notes, and a toggle lets you mark notes as organized.
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={checked} onCheckedChange={onChange} aria-label="Organize notes explicitly" />
|
||||
</label>
|
||||
<SettingsSwitchRow
|
||||
label="Organize notes explicitly"
|
||||
description="When enabled, an Inbox section shows unorganized notes, and a toggle lets you mark notes as organized."
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
testId="settings-explicit-organization"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsSwitchRow({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
onChange,
|
||||
testId,
|
||||
}: {
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
onChange: (value: boolean) => void
|
||||
testId?: string
|
||||
}) {
|
||||
return (
|
||||
<label
|
||||
className="flex items-start justify-between gap-3"
|
||||
style={{ cursor: 'pointer' }}
|
||||
data-testid={testId}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--foreground)' }}>{label}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>{description}</div>
|
||||
</div>
|
||||
<Switch checked={checked} onCheckedChange={onChange} aria-label={label} />
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function TelemetryToggle({
|
||||
label,
|
||||
description,
|
||||
|
||||
32
src/hooks/commands/settingsCommands.test.ts
Normal file
32
src/hooks/commands/settingsCommands.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildSettingsCommands } from './settingsCommands'
|
||||
|
||||
describe('buildSettingsCommands', () => {
|
||||
it('adds a discoverable H1 auto-rename settings command', () => {
|
||||
const onOpenSettings = vi.fn()
|
||||
|
||||
const commands = buildSettingsCommands({ onOpenSettings })
|
||||
const command = commands.find((item) => item.id === 'open-h1-auto-rename-setting')
|
||||
|
||||
expect(command).toMatchObject({
|
||||
label: 'Open H1 Auto-Rename Setting',
|
||||
enabled: true,
|
||||
group: 'Settings',
|
||||
})
|
||||
|
||||
command?.execute()
|
||||
expect(onOpenSettings).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps the general settings command available', () => {
|
||||
const onOpenSettings = vi.fn()
|
||||
|
||||
const commands = buildSettingsCommands({ onOpenSettings })
|
||||
|
||||
expect(commands.find((item) => item.id === 'open-settings')).toMatchObject({
|
||||
label: 'Open Settings',
|
||||
shortcut: '⌘,',
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -15,6 +15,53 @@ interface SettingsCommandsConfig {
|
||||
onRepairVault?: () => void
|
||||
}
|
||||
|
||||
function buildPrimarySettingsCommands({
|
||||
onOpenSettings,
|
||||
onOpenFeedback,
|
||||
onCheckForUpdates,
|
||||
}: Pick<SettingsCommandsConfig, 'onOpenSettings' | 'onOpenFeedback' | 'onCheckForUpdates'>): CommandAction[] {
|
||||
return [
|
||||
{ id: 'open-settings', label: 'Open Settings', group: 'Settings', shortcut: '⌘,', keywords: ['preferences', 'config'], enabled: true, execute: onOpenSettings },
|
||||
{
|
||||
id: 'open-h1-auto-rename-setting',
|
||||
label: 'Open H1 Auto-Rename Setting',
|
||||
group: 'Settings',
|
||||
keywords: ['h1', 'title', 'filename', 'rename', 'auto', 'untitled', 'sync', 'preference'],
|
||||
enabled: true,
|
||||
execute: onOpenSettings,
|
||||
},
|
||||
{ id: 'give-feedback', label: 'Give Feedback', group: 'Settings', keywords: ['feedback', 'issue', 'bug', 'github', 'report'], enabled: !!onOpenFeedback, execute: () => onOpenFeedback?.() },
|
||||
{ id: 'check-updates', label: 'Check for Updates', group: 'Settings', keywords: ['update', 'version', 'upgrade', 'release'], enabled: true, execute: () => onCheckForUpdates?.() },
|
||||
]
|
||||
}
|
||||
|
||||
function buildVaultSettingsCommands({
|
||||
vaultCount,
|
||||
isGettingStartedHidden,
|
||||
onOpenVault,
|
||||
onRemoveActiveVault,
|
||||
onRestoreGettingStarted,
|
||||
}: Pick<SettingsCommandsConfig, 'vaultCount' | 'isGettingStartedHidden' | 'onOpenVault' | 'onRemoveActiveVault' | 'onRestoreGettingStarted'>): CommandAction[] {
|
||||
return [
|
||||
{ id: 'open-vault', label: 'Open Vault…', group: 'Settings', keywords: ['vault', 'folder', 'switch', 'open', 'workspace'], enabled: true, execute: () => onOpenVault?.() },
|
||||
{ id: 'remove-vault', label: 'Remove Vault from List', group: 'Settings', keywords: ['vault', 'remove', 'disconnect', 'hide'], enabled: (vaultCount ?? 0) > 1 && !!onRemoveActiveVault, execute: () => onRemoveActiveVault?.() },
|
||||
{ id: 'restore-getting-started', label: 'Restore Getting Started Vault', group: 'Settings', keywords: ['vault', 'restore', 'demo', 'getting started', 'reset'], enabled: !!isGettingStartedHidden && !!onRestoreGettingStarted, execute: () => onRestoreGettingStarted?.() },
|
||||
]
|
||||
}
|
||||
|
||||
function buildMaintenanceCommands({
|
||||
mcpStatus,
|
||||
onInstallMcp,
|
||||
onReloadVault,
|
||||
onRepairVault,
|
||||
}: Pick<SettingsCommandsConfig, 'mcpStatus' | 'onInstallMcp' | 'onReloadVault' | 'onRepairVault'>): CommandAction[] {
|
||||
return [
|
||||
{ id: 'install-mcp', label: mcpStatus === 'installed' ? 'Restore MCP Server' : 'Install MCP Server', group: 'Settings', keywords: ['mcp', 'claude', 'ai', 'tools', 'install', 'restore', 'fix', 'repair'], enabled: true, execute: () => onInstallMcp?.() },
|
||||
{ id: 'reload-vault', label: 'Reload Vault', group: 'Settings', keywords: ['reload', 'refresh', 'rescan', 'sync', 'filesystem', 'cache'], enabled: !!onReloadVault, execute: () => onReloadVault?.() },
|
||||
{ id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset', 'flatten', 'structure'], enabled: !!onRepairVault, execute: () => onRepairVault?.() },
|
||||
]
|
||||
}
|
||||
|
||||
export function buildSettingsCommands(config: SettingsCommandsConfig): CommandAction[] {
|
||||
const {
|
||||
mcpStatus, vaultCount, isGettingStartedHidden,
|
||||
@@ -23,14 +70,14 @@ export function buildSettingsCommands(config: SettingsCommandsConfig): CommandAc
|
||||
} = config
|
||||
|
||||
return [
|
||||
{ id: 'open-settings', label: 'Open Settings', group: 'Settings', shortcut: '⌘,', keywords: ['preferences', 'config'], enabled: true, execute: onOpenSettings },
|
||||
{ id: 'give-feedback', label: 'Give Feedback', group: 'Settings', keywords: ['feedback', 'issue', 'bug', 'github', 'report'], enabled: !!onOpenFeedback, execute: () => onOpenFeedback?.() },
|
||||
{ id: 'open-vault', label: 'Open Vault…', group: 'Settings', keywords: ['vault', 'folder', 'switch', 'open', 'workspace'], enabled: true, execute: () => onOpenVault?.() },
|
||||
{ id: 'remove-vault', label: 'Remove Vault from List', group: 'Settings', keywords: ['vault', 'remove', 'disconnect', 'hide'], enabled: (vaultCount ?? 0) > 1 && !!onRemoveActiveVault, execute: () => onRemoveActiveVault?.() },
|
||||
{ id: 'restore-getting-started', label: 'Restore Getting Started Vault', group: 'Settings', keywords: ['vault', 'restore', 'demo', 'getting started', 'reset'], enabled: !!isGettingStartedHidden && !!onRestoreGettingStarted, execute: () => onRestoreGettingStarted?.() },
|
||||
{ id: 'check-updates', label: 'Check for Updates', group: 'Settings', keywords: ['update', 'version', 'upgrade', 'release'], enabled: true, execute: () => onCheckForUpdates?.() },
|
||||
{ id: 'install-mcp', label: mcpStatus === 'installed' ? 'Restore MCP Server' : 'Install MCP Server', group: 'Settings', keywords: ['mcp', 'claude', 'ai', 'tools', 'install', 'restore', 'fix', 'repair'], enabled: true, execute: () => onInstallMcp?.() },
|
||||
{ id: 'reload-vault', label: 'Reload Vault', group: 'Settings', keywords: ['reload', 'refresh', 'rescan', 'sync', 'filesystem', 'cache'], enabled: !!onReloadVault, execute: () => onReloadVault?.() },
|
||||
{ id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset', 'flatten', 'structure'], enabled: !!onRepairVault, execute: () => onRepairVault?.() },
|
||||
...buildPrimarySettingsCommands({ onOpenSettings, onOpenFeedback, onCheckForUpdates }),
|
||||
...buildVaultSettingsCommands({
|
||||
vaultCount,
|
||||
isGettingStartedHidden,
|
||||
onOpenVault,
|
||||
onRemoveActiveVault,
|
||||
onRestoreGettingStarted,
|
||||
}),
|
||||
...buildMaintenanceCommands({ mcpStatus, onInstallMcp, onReloadVault, onRepairVault }),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ describe('useAppSave', () => {
|
||||
handleRenameFilename: vi.fn().mockResolvedValue(undefined),
|
||||
replaceEntry: vi.fn(),
|
||||
resolvedPath: '/vault',
|
||||
initialH1AutoRenameEnabled: true,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -57,6 +58,7 @@ describe('useAppSave', () => {
|
||||
deps.activeTabPath = null
|
||||
deps.handleRenameNote.mockResolvedValue(undefined)
|
||||
deps.handleRenameFilename.mockResolvedValue(undefined)
|
||||
deps.initialH1AutoRenameEnabled = true
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -216,6 +218,35 @@ describe('useAppSave', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('does not auto-rename untitled notes when the H1 auto-rename preference is disabled', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.mocked(isTauri).mockReturnValue(true)
|
||||
vi.mocked(invoke).mockImplementation(async (command: string) => {
|
||||
if (command === 'save_note_content') return undefined
|
||||
if (command === 'auto_rename_untitled') {
|
||||
throw new Error('auto_rename_untitled should not run when disabled')
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const entry = makeEntry('/vault/untitled-note-123.md', 'Untitled Note 123', 'untitled-note-123.md')
|
||||
const tabs = [{ entry, content: '# Fresh Title\n\nBody' }]
|
||||
const { result } = renderSave({
|
||||
tabs,
|
||||
activeTabPath: entry.path,
|
||||
unsavedPaths: new Set([entry.path]),
|
||||
initialH1AutoRenameEnabled: false,
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleContentChange(entry.path, '# Fresh Title\n\nBody')
|
||||
await vi.advanceTimersByTimeAsync(3_000)
|
||||
})
|
||||
|
||||
expect(vi.mocked(invoke)).not.toHaveBeenCalledWith('auto_rename_untitled', expect.anything())
|
||||
expect(deps.replaceEntry).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('switches the active tab to the renamed path after untitled H1 auto-rename', async () => {
|
||||
const { result, newPath, getTabs } = setupUntitledRenameHarness()
|
||||
|
||||
|
||||
@@ -97,8 +97,17 @@ function isUntitledRenameCandidate(path: string): boolean {
|
||||
return stem.startsWith('untitled-') && /\d+$/.test(stem)
|
||||
}
|
||||
|
||||
function shouldScheduleUntitledRename({ path, content }: { path: string; content: string }): boolean {
|
||||
function shouldScheduleUntitledRename({
|
||||
path,
|
||||
content,
|
||||
initialH1AutoRenameEnabled,
|
||||
}: {
|
||||
path: string
|
||||
content: string
|
||||
initialH1AutoRenameEnabled: boolean
|
||||
}): boolean {
|
||||
return isTauri()
|
||||
&& initialH1AutoRenameEnabled
|
||||
&& isUntitledRenameCandidate(path)
|
||||
&& extractH1TitleFromContent(content) !== null
|
||||
}
|
||||
@@ -318,8 +327,10 @@ function useUntitledRenameExecutor({
|
||||
|
||||
function useUntitledRenameScheduler({
|
||||
executeUntitledRename,
|
||||
initialH1AutoRenameEnabled,
|
||||
}: {
|
||||
executeUntitledRename: (path: string) => Promise<boolean>
|
||||
initialH1AutoRenameEnabled: boolean
|
||||
}) {
|
||||
const pendingUntitledRenameRef = useRef<PendingUntitledRename | null>(null)
|
||||
|
||||
@@ -334,7 +345,7 @@ function useUntitledRenameScheduler({
|
||||
}, [executeUntitledRename])
|
||||
|
||||
const scheduleUntitledRename = useCallback((path: string, content: string) => {
|
||||
if (!shouldScheduleUntitledRename({ path, content })) {
|
||||
if (!shouldScheduleUntitledRename({ path, content, initialH1AutoRenameEnabled })) {
|
||||
cancelPendingUntitledRename(path)
|
||||
return
|
||||
}
|
||||
@@ -346,7 +357,7 @@ function useUntitledRenameScheduler({
|
||||
void executeUntitledRename(pendingPath)
|
||||
},
|
||||
})
|
||||
}, [cancelPendingUntitledRename, executeUntitledRename])
|
||||
}, [cancelPendingUntitledRename, executeUntitledRename, initialH1AutoRenameEnabled])
|
||||
|
||||
return {
|
||||
pendingUntitledRenameRef,
|
||||
@@ -364,6 +375,7 @@ function useUntitledRenameCoordinator({
|
||||
handleSwitchTab,
|
||||
replaceEntry,
|
||||
loadModifiedFiles,
|
||||
initialH1AutoRenameEnabled,
|
||||
}: {
|
||||
resolvedPath: string
|
||||
tabsRef: MutableRefObject<TabState[]>
|
||||
@@ -372,6 +384,7 @@ function useUntitledRenameCoordinator({
|
||||
handleSwitchTab: AppSaveDeps['handleSwitchTab']
|
||||
replaceEntry: AppSaveDeps['replaceEntry']
|
||||
loadModifiedFiles: AppSaveDeps['loadModifiedFiles']
|
||||
initialH1AutoRenameEnabled: boolean
|
||||
}) {
|
||||
const {
|
||||
renamedPathsRef,
|
||||
@@ -396,7 +409,7 @@ function useUntitledRenameCoordinator({
|
||||
cancelPendingUntitledRename,
|
||||
flushPendingUntitledRename,
|
||||
scheduleUntitledRename,
|
||||
} = useUntitledRenameScheduler({ executeUntitledRename })
|
||||
} = useUntitledRenameScheduler({ executeUntitledRename, initialH1AutoRenameEnabled })
|
||||
|
||||
return {
|
||||
pendingUntitledRenameRef,
|
||||
@@ -424,6 +437,7 @@ interface AppSaveDeps {
|
||||
handleRenameFilename: (path: string, newFilenameStem: string, vaultPath: string, onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void) => Promise<void>
|
||||
replaceEntry: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void
|
||||
resolvedPath: string
|
||||
initialH1AutoRenameEnabled: boolean
|
||||
}
|
||||
|
||||
function useAppSaveStateRefs({
|
||||
@@ -754,6 +768,7 @@ export function useAppSave({
|
||||
updateEntry, setTabs, handleSwitchTab, setToastMessage, loadModifiedFiles, reloadViews,
|
||||
clearUnsaved, unsavedPaths, tabs, activeTabPath, handleRenameNote,
|
||||
handleRenameFilename: handleRenameFilenameRaw, replaceEntry, resolvedPath,
|
||||
initialH1AutoRenameEnabled,
|
||||
}: AppSaveDeps) {
|
||||
const contentChangeRef = useRef<(path: string, content: string) => void>(() => {})
|
||||
const { tabsRef, activeTabPathRef, unsavedPathsRef } = useAppSaveStateRefs({ tabs, activeTabPath, unsavedPaths })
|
||||
@@ -768,6 +783,7 @@ export function useAppSave({
|
||||
handleSwitchTab,
|
||||
replaceEntry,
|
||||
loadModifiedFiles,
|
||||
initialH1AutoRenameEnabled,
|
||||
})
|
||||
const { handleSaveRaw, handleContentChange, savePendingForPath, savePending } = useEditorPersistence({
|
||||
updateEntry,
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface Settings {
|
||||
analytics_enabled: boolean | null
|
||||
anonymous_id: string | null
|
||||
release_channel: string | null
|
||||
initial_h1_auto_rename_enabled?: boolean | null
|
||||
default_ai_agent?: AiAgentId | null
|
||||
}
|
||||
|
||||
|
||||
62
tests/smoke/h1-auto-rename-setting.spec.ts
Normal file
62
tests/smoke/h1-auto-rename-setting.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
import { createFixtureVaultCopy, openFixtureVaultTauri, removeFixtureVaultCopy } from '../helpers/fixtureVault'
|
||||
import { openCommandPalette, executeCommand } from './helpers'
|
||||
import { triggerMenuCommand } from './testBridge'
|
||||
|
||||
let tempVaultDir: string
|
||||
|
||||
async function createUntitledNote(page: Page): Promise<void> {
|
||||
await page.locator('body').click()
|
||||
await triggerMenuCommand(page, 'file-new-note')
|
||||
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testInfo.setTimeout(90_000)
|
||||
tempVaultDir = createFixtureVaultCopy()
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
removeFixtureVaultCopy(tempVaultDir)
|
||||
})
|
||||
|
||||
test('@smoke disabling H1 auto-rename keeps untitled filenames until manual sync', async ({ page }) => {
|
||||
await openCommandPalette(page)
|
||||
await executeCommand(page, 'H1 Auto-Rename')
|
||||
|
||||
const settingsPanel = page.getByTestId('settings-panel')
|
||||
await expect(settingsPanel).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
const autoRenameSwitch = page.getByRole('switch', { name: 'Auto-rename untitled notes from first H1' })
|
||||
await expect(autoRenameSwitch).toHaveAttribute('aria-checked', 'true')
|
||||
await autoRenameSwitch.focus()
|
||||
await page.keyboard.press('Space')
|
||||
await expect(autoRenameSwitch).toHaveAttribute('aria-checked', 'false')
|
||||
await page.keyboard.press('Meta+Enter')
|
||||
await expect(settingsPanel).not.toBeVisible({ timeout: 5_000 })
|
||||
|
||||
await createUntitledNote(page)
|
||||
|
||||
const filenameTrigger = page.getByTestId('breadcrumb-filename-trigger')
|
||||
const untitledStem = (await filenameTrigger.textContent())?.trim() ?? ''
|
||||
expect(untitledStem).toMatch(/^untitled-note-\d+(?:-\d+)?$/i)
|
||||
|
||||
await page.keyboard.type('Manual Sync Title', { delay: 20 })
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(2_700)
|
||||
|
||||
await expect(filenameTrigger).toContainText(untitledStem)
|
||||
await expect(page.getByTestId('breadcrumb-sync-button')).toBeVisible()
|
||||
await expect.poll(() => fs.existsSync(path.join(tempVaultDir, `${untitledStem}.md`))).toBe(true)
|
||||
|
||||
const syncButton = page.getByTestId('breadcrumb-sync-button')
|
||||
await syncButton.focus()
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(filenameTrigger).toContainText('manual-sync-title')
|
||||
await expect.poll(() => fs.existsSync(path.join(tempVaultDir, `${untitledStem}.md`))).toBe(false)
|
||||
await expect.poll(() => fs.existsSync(path.join(tempVaultDir, 'manual-sync-title.md'))).toBe(true)
|
||||
})
|
||||
Reference in New Issue
Block a user