diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 87d5ab92..b64daeb6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -697,7 +697,7 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks: | `App.tsx` | `selection`, panel widths, dialog visibility, toast, view mode | UI state | | `useVaultLoader` | `entries`, `allContent`, `modifiedFiles` | Vault data | | `useNoteActions` | `tabs`, `activeTabPath` | Composes `useNoteCreation` + `useNoteRename` + `frontmatterOps` | -| `useNoteCreation` | — | Note/type/daily-note creation with optimistic persistence | +| `useNoteCreation` | — | Note/type creation with optimistic persistence | | `useNoteRename` | — | Note renaming with wikilink update | | `frontmatterOps` | — (pure functions) | Frontmatter CRUD: key→VaultEntry mapping, mock/Tauri dispatch | | `useTabManagement` | Navigation history, note switching | Note navigation lifecycle | diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index fee993d0..465b9d09 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -82,7 +82,7 @@ tolaria/ │ │ ├── useVaultSwitcher.ts # Multi-vault management │ │ ├── useVaultConfig.ts # Per-vault UI settings │ │ ├── useNoteActions.ts # Composes creation + rename + frontmatter -│ │ ├── useNoteCreation.ts # Note/type/daily-note creation +│ │ ├── useNoteCreation.ts # Note/type creation │ │ ├── useNoteRename.ts # Note renaming + wikilink updates │ │ ├── useAiAgent.ts # AI agent state + tool tracking │ │ ├── useAiActivity.ts # MCP UI bridge listener diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 3d51d324..4ad655a2 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -9,7 +9,6 @@ const APP_CHECK_FOR_UPDATES: &str = "app-check-for-updates"; const FILE_NEW_NOTE: &str = "file-new-note"; const FILE_NEW_TYPE: &str = "file-new-type"; -const FILE_DAILY_NOTE: &str = "file-daily-note"; const FILE_QUICK_OPEN: &str = "file-quick-open"; const FILE_SAVE: &str = "file-save"; @@ -57,7 +56,6 @@ const CUSTOM_IDS: &[&str] = &[ APP_CHECK_FOR_UPDATES, FILE_NEW_NOTE, FILE_NEW_TYPE, - FILE_DAILY_NOTE, FILE_QUICK_OPEN, FILE_SAVE, EDIT_FIND_IN_VAULT, @@ -153,10 +151,6 @@ fn build_file_menu(app: &App) -> MenuResult { let new_type = MenuItemBuilder::new("New Type") .id(FILE_NEW_TYPE) .build(app)?; - let daily_note = MenuItemBuilder::new("Open Today's Note") - .id(FILE_DAILY_NOTE) - .accelerator("CmdOrCtrl+J") - .build(app)?; let quick_open = MenuItemBuilder::new("Quick Open") .id(FILE_QUICK_OPEN) .accelerator("CmdOrCtrl+P") @@ -168,7 +162,6 @@ fn build_file_menu(app: &App) -> MenuResult { Ok(SubmenuBuilder::new(app, "File") .item(&new_note) .item(&new_type) - .item(&daily_note) .item(&quick_open) .separator() .item(&save) diff --git a/src/App.tsx b/src/App.tsx index 8bdb5020..c92d99f5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -562,7 +562,6 @@ function App() { onQuickOpen: dialogs.openQuickOpen, onCommandPalette: dialogs.openCommandPalette, onSearch: dialogs.openSearch, onCreateNote: notes.handleCreateNoteImmediate, - onOpenDailyNote: notes.handleOpenDailyNote, onCreateNoteOfType: notes.handleCreateNoteImmediate, onSave: appSave.handleSave, onOpenSettings: dialogs.openSettings, diff --git a/src/hooks/appCommandCatalog.ts b/src/hooks/appCommandCatalog.ts index eb354769..a8ae266f 100644 --- a/src/hooks/appCommandCatalog.ts +++ b/src/hooks/appCommandCatalog.ts @@ -6,7 +6,6 @@ export const APP_COMMAND_IDS = { appCheckForUpdates: 'app-check-for-updates', fileNewNote: 'file-new-note', fileNewType: 'file-new-type', - fileDailyNote: 'file-daily-note', fileQuickOpen: 'file-quick-open', fileSave: 'file-save', editFindInVault: 'edit-find-in-vault', @@ -77,7 +76,6 @@ type SimpleHandlerKey = | 'onCheckForUpdates' | 'onCreateNote' | 'onCreateType' - | 'onOpenDailyNote' | 'onQuickOpen' | 'onSave' | 'onSearch' @@ -150,11 +148,6 @@ export const APP_COMMAND_DEFINITIONS: Record route: { kind: 'handler', handler: 'onCreateType' }, menuOwned: true, }, - [APP_COMMAND_IDS.fileDailyNote]: { - route: { kind: 'handler', handler: 'onOpenDailyNote' }, - menuOwned: true, - shortcut: { combo: 'command-or-ctrl', key: 'j', code: 'KeyJ', display: '⌘J' }, - }, [APP_COMMAND_IDS.fileQuickOpen]: { route: { kind: 'handler', handler: 'onQuickOpen' }, menuOwned: true, @@ -336,7 +329,6 @@ const NATIVE_MENU_COMMAND_SET = new Set( const MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET = new Set([ APP_COMMAND_IDS.appSettings, APP_COMMAND_IDS.fileNewNote, - APP_COMMAND_IDS.fileDailyNote, APP_COMMAND_IDS.fileQuickOpen, APP_COMMAND_IDS.fileSave, APP_COMMAND_IDS.editFindInVault, diff --git a/src/hooks/appCommandDispatcher.test.ts b/src/hooks/appCommandDispatcher.test.ts index d65ff50f..d14f3c19 100644 --- a/src/hooks/appCommandDispatcher.test.ts +++ b/src/hooks/appCommandDispatcher.test.ts @@ -20,7 +20,6 @@ function makeHandlers(): AppCommandHandlers { onSetViewMode: vi.fn(), onCreateNote: vi.fn(), onCreateType: vi.fn(), - onOpenDailyNote: vi.fn(), onQuickOpen: vi.fn(), onSave: vi.fn(), onOpenSettings: vi.fn(), @@ -67,6 +66,10 @@ describe('appCommandDispatcher', () => { expect(isAppCommandId('not-a-command')).toBe(false) }) + it('no longer recognizes the removed daily-note menu id', () => { + expect(isAppCommandId('file-daily-note')).toBe(false) + }) + it('distinguishes native menu ids from keyboard-only ids', () => { expect(isNativeMenuCommandId(APP_COMMAND_IDS.fileNewNote)).toBe(true) expect(isNativeMenuCommandId(APP_COMMAND_IDS.noteToggleFavorite)).toBe(false) diff --git a/src/hooks/appCommandDispatcher.ts b/src/hooks/appCommandDispatcher.ts index 31770340..eeb7b91c 100644 --- a/src/hooks/appCommandDispatcher.ts +++ b/src/hooks/appCommandDispatcher.ts @@ -28,7 +28,6 @@ export interface AppCommandHandlers { onSetViewMode: (mode: ViewMode) => void onCreateNote: () => void onCreateType?: () => void - onOpenDailyNote: () => void onQuickOpen: () => void onSave: () => void onOpenSettings: () => void @@ -70,7 +69,6 @@ type SimpleHandlerKey = keyof Pick< | 'onCheckForUpdates' | 'onCreateNote' | 'onCreateType' - | 'onOpenDailyNote' | 'onQuickOpen' | 'onSave' | 'onSearch' @@ -108,7 +106,6 @@ const SIMPLE_HANDLER_EXECUTORS: Record handlers.onCheckForUpdates?.(), onCreateNote: (handlers) => handlers.onCreateNote(), onCreateType: (handlers) => handlers.onCreateType?.(), - onOpenDailyNote: (handlers) => handlers.onOpenDailyNote(), onQuickOpen: (handlers) => handlers.onQuickOpen(), onSave: (handlers) => handlers.onSave(), onSearch: (handlers) => handlers.onSearch(), diff --git a/src/hooks/appKeyboardShortcuts.ts b/src/hooks/appKeyboardShortcuts.ts index 1997b6c2..dbe3324e 100644 --- a/src/hooks/appKeyboardShortcuts.ts +++ b/src/hooks/appKeyboardShortcuts.ts @@ -12,7 +12,6 @@ export type KeyboardActions = Pick< | 'onCommandPalette' | 'onSearch' | 'onCreateNote' - | 'onOpenDailyNote' | 'onSave' | 'onOpenSettings' | 'onDeleteNote' diff --git a/src/hooks/commands/navigationCommands.ts b/src/hooks/commands/navigationCommands.ts index 8247a3bf..c6a6313b 100644 --- a/src/hooks/commands/navigationCommands.ts +++ b/src/hooks/commands/navigationCommands.ts @@ -5,7 +5,6 @@ interface NavigationCommandsConfig { onQuickOpen: () => void onSelect: (sel: SidebarSelection) => void showInbox?: boolean - onOpenDailyNote: () => void onGoBack?: () => void onGoForward?: () => void canGoBack?: boolean diff --git a/src/hooks/commands/noteCommands.ts b/src/hooks/commands/noteCommands.ts index f5c13354..446bb280 100644 --- a/src/hooks/commands/noteCommands.ts +++ b/src/hooks/commands/noteCommands.ts @@ -7,7 +7,6 @@ interface NoteCommandsConfig { activeNoteHasIcon?: boolean onCreateNote: () => void onCreateType?: () => void - onOpenDailyNote: () => void onSave: () => void onDeleteNote: (path: string) => void onArchiveNote: (path: string) => void @@ -23,10 +22,34 @@ interface NoteCommandsConfig { canRestoreDeletedNote?: boolean } +interface ActivePathCommandConfig { + id: string + label: string + keywords: string[] + enabled: boolean + path: string | null + shortcut?: string + run: (path: string) => void +} + +function createActivePathCommand(config: ActivePathCommandConfig): CommandAction { + return { + id: config.id, + label: config.label, + group: 'Note', + shortcut: config.shortcut, + keywords: config.keywords, + enabled: config.enabled, + execute: () => { + if (config.path) config.run(config.path) + }, + } +} + export function buildNoteCommands(config: NoteCommandsConfig): CommandAction[] { const { hasActiveNote, activeTabPath, isArchived, - onCreateNote, onCreateType, onOpenDailyNote, onSave, + onCreateNote, onCreateType, onSave, onDeleteNote, onArchiveNote, onUnarchiveNote, onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow, onToggleFavorite, isFavorite, @@ -37,36 +60,48 @@ export function buildNoteCommands(config: NoteCommandsConfig): CommandAction[] { return [ { id: 'create-note', label: 'New Note', group: 'Note', shortcut: '⌘N', keywords: ['new', 'create', 'add'], enabled: true, execute: onCreateNote }, { id: 'create-type', label: 'New Type', group: 'Note', keywords: ['new', 'create', 'type', 'template'], enabled: !!onCreateType, execute: () => onCreateType?.() }, - { id: 'open-daily-note', label: "Open Today's Note", group: 'Note', shortcut: '⌘J', keywords: ['daily', 'journal', 'today'], enabled: true, execute: onOpenDailyNote }, { id: 'save-note', label: 'Save Note', group: 'Note', shortcut: '⌘S', keywords: ['write'], enabled: hasActiveNote, execute: onSave }, - { - id: 'delete-note', label: 'Delete Note', group: 'Note', shortcut: '⌘⌫', - keywords: ['delete', 'remove'], enabled: hasActiveNote, - execute: () => { if (activeTabPath) onDeleteNote(activeTabPath) }, - }, - { - id: 'archive-note', label: isArchived ? 'Unarchive Note' : 'Archive Note', group: 'Note', - keywords: ['archive'], enabled: hasActiveNote, - execute: () => { if (activeTabPath) (isArchived ? onUnarchiveNote : onArchiveNote)(activeTabPath) }, - }, + createActivePathCommand({ + id: 'delete-note', + label: 'Delete Note', + shortcut: '⌘⌫', + keywords: ['delete', 'remove'], + enabled: hasActiveNote, + path: activeTabPath, + run: onDeleteNote, + }), + createActivePathCommand({ + id: 'archive-note', + label: isArchived ? 'Unarchive Note' : 'Archive Note', + keywords: ['archive'], + enabled: hasActiveNote, + path: activeTabPath, + run: isArchived ? onUnarchiveNote : onArchiveNote, + }), { id: 'restore-deleted-note', label: 'Restore Deleted Note', group: 'Note', keywords: ['restore', 'deleted', 'undelete', 'git', 'checkout'], enabled: !!canRestoreDeletedNote && !!onRestoreDeletedNote, execute: () => onRestoreDeletedNote?.(), }, - { - id: 'toggle-favorite', label: isFavorite ? 'Remove from Favorites' : 'Add to Favorites', group: 'Note', shortcut: '⌘D', + createActivePathCommand({ + id: 'toggle-favorite', + label: isFavorite ? 'Remove from Favorites' : 'Add to Favorites', + shortcut: '⌘D', keywords: ['favorite', 'star', 'bookmark', 'pin'], enabled: hasActiveNote && !!onToggleFavorite, - execute: () => { if (activeTabPath) onToggleFavorite?.(activeTabPath) }, - }, - { - id: 'toggle-organized', label: isOrganized ? 'Mark as Unorganized' : 'Mark as Organized', group: 'Note', shortcut: '⌘E', + path: activeTabPath, + run: (path) => onToggleFavorite?.(path), + }), + createActivePathCommand({ + id: 'toggle-organized', + label: isOrganized ? 'Mark as Unorganized' : 'Mark as Organized', + shortcut: '⌘E', keywords: ['organized', 'inbox', 'triage', 'done'], enabled: hasActiveNote && !!onToggleOrganized, - execute: () => { if (activeTabPath) onToggleOrganized?.(activeTabPath) }, - }, + path: activeTabPath, + run: (path) => onToggleOrganized?.(path), + }), { id: 'set-note-icon', label: 'Set Note Icon', group: 'Note', keywords: ['icon', 'emoji', 'set', 'add', 'change', 'picker'], diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts index 327ed09b..357b4659 100644 --- a/src/hooks/useAppCommands.ts +++ b/src/hooks/useAppCommands.ts @@ -19,7 +19,6 @@ interface AppCommandsConfig { onCommandPalette: () => void onSearch: () => void onCreateNote: () => void - onOpenDailyNote: () => void onCreateNoteOfType: (type: string) => void onSave: () => void onOpenSettings: () => void @@ -75,39 +74,17 @@ interface AppCommandsConfig { canRestoreDeletedNote?: boolean } -/** Sets up keyboard shortcuts, command registry, menu events, and keyboard navigation. */ -export function useAppCommands(config: AppCommandsConfig): CommandAction[] { - const entriesRef = useRef(config.entries) - // eslint-disable-next-line react-hooks/refs - entriesRef.current = config.entries - - const toggleArchive = useCallback((path: string) => { - const entry = entriesRef.current.find(e => e.path === path) - ;(entry?.archived ? config.onUnarchiveNote : config.onArchiveNote)(path) - }, [config.onArchiveNote, config.onUnarchiveNote]) - - - const { onSelect } = config - - const selectFilter = useCallback((filter: SidebarFilter) => { - const safeFilter = !config.showInbox && filter === 'inbox' ? 'all' : filter - onSelect({ kind: 'filter', filter: safeFilter }) - }, [config.showInbox, onSelect]) - - const viewChanges = useCallback(() => { - onSelect({ kind: 'filter', filter: 'changes' }) - }, [onSelect]) - - useAppKeyboard({ +function createKeyboardActions( + config: AppCommandsConfig, +): Omit[0], 'onArchiveNote'> { + return { onQuickOpen: config.onQuickOpen, onCommandPalette: config.onCommandPalette, onSearch: config.onSearch, onCreateNote: config.onCreateNote, - onOpenDailyNote: config.onOpenDailyNote, onSave: config.onSave, onOpenSettings: config.onOpenSettings, onDeleteNote: config.onDeleteNote, - onArchiveNote: toggleArchive, onSetViewMode: config.onSetViewMode, onZoomIn: config.onZoomIn, onZoomOut: config.onZoomOut, @@ -121,13 +98,18 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onToggleOrganized: config.onToggleOrganized, onOpenInNewWindow: config.onOpenInNewWindow, activeTabPathRef: config.activeTabPathRef, - }) + } +} - useMenuEvents({ +function createMenuEventHandlers( + config: AppCommandsConfig, + selectFilter: (filter: SidebarFilter) => void, + viewChanges: () => void, +): Omit[0], 'onArchiveNote'> { + return { onSetViewMode: config.onSetViewMode, onCreateNote: config.onCreateNote, onCreateType: config.onCreateType, - onOpenDailyNote: config.onOpenDailyNote, onQuickOpen: config.onQuickOpen, onSave: config.onSave, onOpenSettings: config.onOpenSettings, @@ -136,7 +118,6 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onZoomIn: config.onZoomIn, onZoomOut: config.onZoomOut, onZoomReset: config.onZoomReset, - onArchiveNote: toggleArchive, onDeleteNote: config.onDeleteNote, onSearch: config.onSearch, onToggleRawEditor: config.onToggleRawEditor, @@ -163,9 +144,11 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { activeTabPath: config.activeTabPath, modifiedCount: config.modifiedCount, hasRestorableDeletedNote: config.canRestoreDeletedNote, - }) + } +} - const commands = useCommandRegistry({ +function createCommandRegistryConfig(config: AppCommandsConfig): Parameters[0] { + return { activeTabPath: config.activeTabPath, entries: config.entries, modifiedCount: config.modifiedCount, @@ -194,7 +177,6 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { zoomLevel: config.zoomLevel, onSelect: config.onSelect, showInbox: config.showInbox, - onOpenDailyNote: config.onOpenDailyNote, onGoBack: config.onGoBack, onGoForward: config.onGoForward, canGoBack: config.canGoBack, @@ -222,7 +204,40 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { canCustomizeInboxColumns: config.canCustomizeInboxColumns, onRestoreDeletedNote: config.onRestoreDeletedNote, canRestoreDeletedNote: config.canRestoreDeletedNote, - }) + } +} + +/** Sets up keyboard shortcuts, command registry, menu events, and keyboard navigation. */ +export function useAppCommands(config: AppCommandsConfig): CommandAction[] { + const entriesRef = useRef(config.entries) + // eslint-disable-next-line react-hooks/refs + entriesRef.current = config.entries + + const toggleArchive = useCallback((path: string) => { + const entry = entriesRef.current.find(e => e.path === path) + ;(entry?.archived ? config.onUnarchiveNote : config.onArchiveNote)(path) + }, [config.onArchiveNote, config.onUnarchiveNote]) + + + const { onSelect } = config + + const selectFilter = useCallback((filter: SidebarFilter) => { + const safeFilter = !config.showInbox && filter === 'inbox' ? 'all' : filter + onSelect({ kind: 'filter', filter: safeFilter }) + }, [config.showInbox, onSelect]) + + const viewChanges = useCallback(() => { + onSelect({ kind: 'filter', filter: 'changes' }) + }, [onSelect]) + + const keyboardActions = createKeyboardActions(config) + const menuEventHandlers = createMenuEventHandlers(config, selectFilter, viewChanges) + + useAppKeyboard({ ...keyboardActions, onArchiveNote: toggleArchive }) + + useMenuEvents({ ...menuEventHandlers, onArchiveNote: toggleArchive }) + + const commands = useCommandRegistry(createCommandRegistryConfig(config)) useKeyboardNavigation({ activeTabPath: config.activeTabPath, diff --git a/src/hooks/useAppKeyboard.test.ts b/src/hooks/useAppKeyboard.test.ts index 866460f5..7d47e566 100644 --- a/src/hooks/useAppKeyboard.test.ts +++ b/src/hooks/useAppKeyboard.test.ts @@ -34,7 +34,6 @@ function makeActions() { onCommandPalette: vi.fn(), onSearch: vi.fn(), onCreateNote: vi.fn(), - onOpenDailyNote: vi.fn(), onSave: vi.fn(), onOpenSettings: vi.fn(), onDeleteNote: vi.fn(), @@ -169,11 +168,12 @@ describe('useAppKeyboard', () => { }) }) - it('Cmd+J triggers open daily note', () => { + it('Cmd+J no longer triggers any app command', () => { const actions = makeActions() renderHook(() => useAppKeyboard(actions)) fireKey('j', { metaKey: true }) - expect(actions.onOpenDailyNote).toHaveBeenCalled() + expect(actions.onCreateNote).not.toHaveBeenCalled() + expect(actions.onQuickOpen).not.toHaveBeenCalled() }) it('Alt+4 does not trigger any view mode', () => { diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts index 14936ac8..bc346e58 100644 --- a/src/hooks/useCommandRegistry.test.ts +++ b/src/hooks/useCommandRegistry.test.ts @@ -31,7 +31,6 @@ function makeConfig(overrides: Record = {}) { onZoomReset: vi.fn(), zoomLevel: 100, onSelect: vi.fn(), - onOpenDailyNote: vi.fn(), onCloseTab: vi.fn(), onGoBack: vi.fn(), onGoForward: vi.fn(), @@ -207,6 +206,12 @@ describe('useCommandRegistry', () => { expect(findCommand(result.current, 'go-inbox')).toBeUndefined() }) + it('omits the removed daily-note command', () => { + const config = makeConfig() + const { result } = renderHook(() => useCommandRegistry(config)) + expect(findCommand(result.current, 'open-daily-note')).toBeUndefined() + }) + it('includes Give Feedback in the Settings group when available', () => { const onOpenFeedback = vi.fn() const config = makeConfig({ onOpenFeedback }) diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts index a0f96586..9742790f 100644 --- a/src/hooks/useCommandRegistry.ts +++ b/src/hooks/useCommandRegistry.ts @@ -60,7 +60,6 @@ interface CommandRegistryConfig { onZoomReset: () => void zoomLevel: number onSelect: (sel: SidebarSelection) => void - onOpenDailyNote: () => void showInbox?: boolean onGoBack?: () => void onGoForward?: () => void @@ -83,7 +82,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, activeNoteModified, onZoomIn, onZoomOut, onZoomReset, zoomLevel, - onSelect, onOpenDailyNote, + onSelect, showInbox, onGoBack, onGoForward, canGoBack, canGoForward, onCheckForUpdates, onCreateType, @@ -110,10 +109,10 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com const vaultTypes = useMemo(() => extractVaultTypes(entries), [entries]) return useMemo(() => [ - ...buildNavigationCommands({ onQuickOpen, onSelect, onOpenDailyNote, showInbox, onGoBack, onGoForward, canGoBack, canGoForward }), + ...buildNavigationCommands({ onQuickOpen, onSelect, showInbox, onGoBack, onGoForward, canGoBack, canGoForward }), ...buildNoteCommands({ hasActiveNote, activeTabPath, isArchived, - onCreateNote, onCreateType, onOpenDailyNote, onSave, + onCreateNote, onCreateType, onSave, onDeleteNote, onArchiveNote, onUnarchiveNote, onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow, onToggleFavorite, isFavorite, onToggleOrganized, isOrganized: activeEntry?.organized ?? false, @@ -139,7 +138,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, onCheckForUpdates, onZoomIn, onZoomOut, onZoomReset, zoomLevel, - onSelect, onOpenDailyNote, + onSelect, showInbox, onGoBack, onGoForward, canGoBack, canGoForward, vaultTypes, diff --git a/src/hooks/useMenuEvents.test.ts b/src/hooks/useMenuEvents.test.ts index 51cd77e7..49802a20 100644 --- a/src/hooks/useMenuEvents.test.ts +++ b/src/hooks/useMenuEvents.test.ts @@ -23,7 +23,6 @@ function makeHandlers(): MenuEventHandlers { onSetViewMode: vi.fn(), onCreateNote: vi.fn(), onCreateType: vi.fn(), - onOpenDailyNote: vi.fn(), onQuickOpen: vi.fn(), onSave: vi.fn(), onOpenSettings: vi.fn(), @@ -117,10 +116,11 @@ describe('dispatchMenuEvent', () => { expect(h.onCreateNote).toHaveBeenCalled() }) - it('file-daily-note triggers open daily note', () => { + it('file-daily-note is ignored once the command is removed', () => { const h = makeHandlers() dispatchMenuEvent('file-daily-note', h) - expect(h.onOpenDailyNote).toHaveBeenCalled() + expect(h.onCreateNote).not.toHaveBeenCalled() + expect(h.onQuickOpen).not.toHaveBeenCalled() }) it('file-quick-open triggers quick open', () => { diff --git a/src/hooks/useNoteActions.hook.test.ts b/src/hooks/useNoteActions.hook.test.ts index 994b3db1..cc000c1e 100644 --- a/src/hooks/useNoteActions.hook.test.ts +++ b/src/hooks/useNoteActions.hook.test.ts @@ -3,7 +3,7 @@ import { renderHook, act } from '@testing-library/react' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import type { VaultEntry } from '../types' -import { RAPID_CREATE_NOTE_SETTLE_MS, todayDateString } from './useNoteCreation' +import { RAPID_CREATE_NOTE_SETTLE_MS } from './useNoteCreation' import { useNoteActions } from './useNoteActions' import type { NoteActionsConfig } from './useNoteActions' @@ -307,34 +307,6 @@ describe('useNoteActions hook', () => { expect(setToastMessage).toHaveBeenCalledWith('Property updated') }) - it('handleOpenDailyNote creates a new daily note when none exists', () => { - const { result } = renderHook(() => useNoteActions(makeConfig())) - - act(() => { - result.current.handleOpenDailyNote() - }) - - expect(addEntry).toHaveBeenCalledTimes(1) - const [createdEntry] = addEntry.mock.calls[0] - expect(createdEntry.isA).toBe('Journal') - expect(createdEntry.path).toMatch(/\/\d{4}-\d{2}-\d{2}\.md$/) - }) - - it('handleOpenDailyNote opens existing daily note instead of creating', async () => { - const today = todayDateString() - const existing = makeEntry({ path: `/Users/luca/Laputa/${today}.md`, filename: `${today}.md`, title: today, isA: 'Journal' }) - const { result } = renderHook(() => useNoteActions(makeConfig([existing]))) - - await act(async () => { - result.current.handleOpenDailyNote() - await new Promise((r) => setTimeout(r, 0)) - }) - - // Should open existing note, not create a new one - expect(addEntry).not.toHaveBeenCalled() - expect(result.current.activeTabPath).toBe(`/Users/luca/Laputa/${today}.md`) - }) - describe('pending save lifecycle', () => { it('createAndPersist calls addPendingSave on start (non-Tauri)', async () => { const addPendingSave = vi.fn() diff --git a/src/hooks/useNoteActions.ts b/src/hooks/useNoteActions.ts index a1d359e4..eed121cc 100644 --- a/src/hooks/useNoteActions.ts +++ b/src/hooks/useNoteActions.ts @@ -46,13 +46,25 @@ interface TitleRenameDeps { updateTabContent: (path: string, content: string) => void } -function applyFrontmatterCallbacks(config: NoteActionsConfig, path: string, newContent: string | undefined): boolean { +interface FrontmatterCallbackParams { + config: NoteActionsConfig + path: string + newContent: string | undefined +} + +function applyFrontmatterCallbacks({ config, path, newContent }: FrontmatterCallbackParams): boolean { if (!newContent) return false config.onFrontmatterContentChanged?.(path, newContent) return true } -async function renameAfterTitleChange(path: string, newTitle: string, deps: TitleRenameDeps): Promise { +interface RenameAfterTitleChangeParams { + path: string + newTitle: string + deps: TitleRenameDeps +} + +async function renameAfterTitleChange({ path, newTitle, deps }: RenameAfterTitleChangeParams): Promise { const oldTitle = deps.tabsRef.current.find(t => t.entry.path === path)?.entry.title const result = await performRename(path, newTitle, deps.vaultPath, oldTitle) if (result.new_path !== path) { @@ -73,21 +85,34 @@ function shouldRenameOnTitleUpdate(key: string, value: FrontmatterValue): value return isTitleKey(key) && typeof value === 'string' && value !== '' } -function navigateWikilink(entries: VaultEntry[], target: string, selectNote: (e: VaultEntry) => void): void { +interface NavigateWikilinkParams { + entries: VaultEntry[] + target: string + selectNote: (entry: VaultEntry) => void +} + +function navigateWikilink({ entries, target, selectNote }: NavigateWikilinkParams): void { const found = resolveEntry(entries, target) if (found) selectNote(found) else console.warn(`Navigation target not found: ${target}`) } -async function maybeRenameAfterFrontmatterUpdate( - path: string, - key: string, - value: FrontmatterValue, - deps: TitleRenameDeps, -): Promise { +interface MaybeRenameAfterFrontmatterUpdateParams { + path: string + key: string + value: FrontmatterValue + deps: TitleRenameDeps +} + +async function maybeRenameAfterFrontmatterUpdate({ + path, + key, + value, + deps, +}: MaybeRenameAfterFrontmatterUpdateParams): Promise { if (!shouldRenameOnTitleUpdate(key, value)) return try { - await renameAfterTitleChange(path, value, deps) + await renameAfterTitleChange({ path, newTitle: value, deps }) } catch (err) { console.error('Failed to rename note after title change:', err) } @@ -102,14 +127,14 @@ export function useNoteActions(config: NoteActionsConfig) { setTabs((prev) => prev.map((t) => t.entry.path === path ? { ...t, content: newContent } : t)) }, [setTabs]) - const creation = useNoteCreation(config, { openTabWithContent, handleSelectNote }) + const creation = useNoteCreation(config, { openTabWithContent }) const rename = useNoteRename( { entries, setToastMessage }, { tabs: tabMgmt.tabs, setTabs, activeTabPathRef, handleSwitchTab, updateTabContent }, ) const handleNavigateWikilink = useCallback( - (target: string) => navigateWikilink(entries, target, handleSelectNote), + (target: string) => navigateWikilink({ entries, target, selectNote: handleSelectNote }), [entries, handleSelectNote], ) @@ -125,26 +150,36 @@ export function useNoteActions(config: NoteActionsConfig) { handleCreateNote: creation.handleCreateNote, handleCreateNoteImmediate: creation.handleCreateNoteImmediate, handleCreateNoteForRelationship: creation.handleCreateNoteForRelationship, - handleOpenDailyNote: creation.handleOpenDailyNote, handleCreateType: creation.handleCreateType, createTypeEntrySilent: creation.createTypeEntrySilent, handleUpdateFrontmatter: useCallback(async (path: string, key: string, value: FrontmatterValue, options?: FrontmatterOpOptions) => { const newContent = await runFrontmatterOp('update', path, key, value, options) - if (!applyFrontmatterCallbacks(config, path, newContent)) return - await maybeRenameAfterFrontmatterUpdate(path, key, value, { - vaultPath: config.vaultPath, tabsRef: rename.tabsRef, replaceEntry: config.replaceEntry, - setTabs, activeTabPathRef, handleSwitchTab, setToastMessage, updateTabContent, + if (!applyFrontmatterCallbacks({ config, path, newContent })) return + await maybeRenameAfterFrontmatterUpdate({ + path, + key, + value, + deps: { + vaultPath: config.vaultPath, + tabsRef: rename.tabsRef, + replaceEntry: config.replaceEntry, + setTabs, + activeTabPathRef, + handleSwitchTab, + setToastMessage, + updateTabContent, + }, }) config.onFrontmatterPersisted?.() }, [runFrontmatterOp, config, rename.tabsRef, setTabs, activeTabPathRef, handleSwitchTab, setToastMessage, updateTabContent]), handleDeleteProperty: useCallback(async (path: string, key: string, options?: FrontmatterOpOptions) => { const newContent = await runFrontmatterOp('delete', path, key, undefined, options) - if (!applyFrontmatterCallbacks(config, path, newContent)) return + if (!applyFrontmatterCallbacks({ config, path, newContent })) return config.onFrontmatterPersisted?.() }, [runFrontmatterOp, config]), handleAddProperty: useCallback(async (path: string, key: string, value: FrontmatterValue) => { const newContent = await runFrontmatterOp('update', path, key, value) - if (!applyFrontmatterCallbacks(config, path, newContent)) return + if (!applyFrontmatterCallbacks({ config, path, newContent })) return config.onFrontmatterPersisted?.() }, [runFrontmatterOp, config]), handleRenameNote: rename.handleRenameNote, diff --git a/src/hooks/useNoteCreation.extra.test.ts b/src/hooks/useNoteCreation.extra.test.ts index fdf3ab8d..7ad907ad 100644 --- a/src/hooks/useNoteCreation.extra.test.ts +++ b/src/hooks/useNoteCreation.extra.test.ts @@ -8,10 +8,6 @@ import { buildNoteContent, resolveNewNote, resolveNewType, - todayDateString, - buildDailyNoteContent, - resolveDailyNote, - findDailyNote, DEFAULT_TEMPLATES, resolveTemplate, } from './useNoteCreation' @@ -319,76 +315,3 @@ describe('resolveNewType', () => { expect(entry.path).not.toContain('/Users/luca/Laputa') }) }) - -describe('todayDateString', () => { - it('returns date in YYYY-MM-DD format', () => { - const result = todayDateString() - expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/) - }) -}) - -describe('buildDailyNoteContent', () => { - it('generates frontmatter with Journal type and date', () => { - const content = buildDailyNoteContent('2026-03-02') - expect(content).toContain('type: Journal') - expect(content).toContain('date: 2026-03-02') - expect(content).toContain('title: 2026-03-02') - }) - - it('includes Intentions and Reflections sections', () => { - const content = buildDailyNoteContent('2026-03-02') - expect(content).toContain('## Intentions') - expect(content).toContain('## Reflections') - }) - - it('does not include H1 heading with the date', () => { - const content = buildDailyNoteContent('2026-03-02') - expect(content).not.toMatch(/^# /m) - }) -}) - -describe('resolveDailyNote', () => { - it('creates entry at vault root with date as filename', () => { - const { entry } = resolveDailyNote('2026-03-02', '/my/vault') - expect(entry.path).toBe('/my/vault/2026-03-02.md') - expect(entry.filename).toBe('2026-03-02.md') - expect(entry.title).toBe('2026-03-02') - expect(entry.isA).toBe('Journal') - expect(entry.status).toBeNull() - }) - - it('returns content with daily note template', () => { - const { content } = resolveDailyNote('2026-03-02', '/my/vault') - expect(content).toContain('type: Journal') - expect(content).toContain('## Intentions') - }) - - it('uses provided vault path', () => { - const { entry } = resolveDailyNote('2026-03-02', '/other/vault') - expect(entry.path).toBe('/other/vault/2026-03-02.md') - }) -}) - -describe('findDailyNote', () => { - it('finds entry by filename and Journal type', () => { - const entries = [ - makeEntry({ path: '/Users/luca/Laputa/2026-03-02.md', filename: '2026-03-02.md', isA: 'Journal' }), - makeEntry({ path: '/Users/luca/Laputa/other.md' }), - ] - const found = findDailyNote(entries, '2026-03-02') - expect(found).toBeDefined() - expect(found!.path).toBe('/Users/luca/Laputa/2026-03-02.md') - }) - - it('returns undefined when no matching entry exists', () => { - const entries = [makeEntry({ path: '/Users/luca/Laputa/other.md' })] - expect(findDailyNote(entries, '2026-03-02')).toBeUndefined() - }) - - it('does not match non-Journal notes with date filename', () => { - const entries = [ - makeEntry({ path: '/vault/2026-03-02.md', filename: '2026-03-02.md', isA: 'Note' }), - ] - expect(findDailyNote(entries, '2026-03-02')).toBeUndefined() - }) -}) diff --git a/src/hooks/useNoteCreation.test.ts b/src/hooks/useNoteCreation.test.ts index 23d823c2..1af3e8e2 100644 --- a/src/hooks/useNoteCreation.test.ts +++ b/src/hooks/useNoteCreation.test.ts @@ -13,10 +13,6 @@ import { resolveNewType, resolveTemplate, DEFAULT_TEMPLATES, - todayDateString, - buildDailyNoteContent, - resolveDailyNote, - findDailyNote, RAPID_CREATE_NOTE_SETTLE_MS, useNoteCreation, } from './useNoteCreation' @@ -188,56 +184,16 @@ describe('resolveTemplate', () => { }) }) -describe('todayDateString', () => { - it('returns date in YYYY-MM-DD format', () => { - expect(todayDateString()).toMatch(/^\d{4}-\d{2}-\d{2}$/) - }) -}) - -describe('buildDailyNoteContent', () => { - it('generates frontmatter with Journal type and date', () => { - const content = buildDailyNoteContent('2026-03-02') - expect(content).toContain('type: Journal') - expect(content).toContain('date: 2026-03-02') - expect(content).toContain('## Intentions') - }) -}) - -describe('resolveDailyNote', () => { - it('creates entry with date as filename', () => { - const { entry } = resolveDailyNote('2026-03-02', '/vault') - expect(entry.path).toBe('/vault/2026-03-02.md') - expect(entry.isA).toBe('Journal') - }) -}) - -describe('findDailyNote', () => { - it('finds entry by filename and Journal type', () => { - const entries = [makeEntry({ filename: '2026-03-02.md', isA: 'Journal' })] - expect(findDailyNote(entries, '2026-03-02')).toBeDefined() - }) - - it('returns undefined when no match', () => { - expect(findDailyNote([], '2026-03-02')).toBeUndefined() - }) - - it('does not match non-Journal notes', () => { - const entries = [makeEntry({ filename: '2026-03-02.md', isA: 'Note' })] - expect(findDailyNote(entries, '2026-03-02')).toBeUndefined() - }) -}) - describe('useNoteCreation hook', () => { const addEntry = vi.fn() const removeEntry = vi.fn() const setToastMessage = vi.fn() const openTabWithContent = vi.fn() - const handleSelectNote = vi.fn() const makeConfig = (entries: VaultEntry[] = []): NoteCreationConfig => ({ addEntry, removeEntry, entries, setToastMessage, vaultPath: '/test/vault', }) - const tabDeps = { openTabWithContent, handleSelectNote } + const tabDeps = { openTabWithContent } beforeEach(() => { vi.clearAllMocks() @@ -373,37 +329,6 @@ describe('useNoteCreation hook', () => { window.removeEventListener('laputa:focus-editor', focusListener) }) - it('handleOpenDailyNote creates new daily note when none exists', () => { - const { result } = renderHook(() => useNoteCreation(makeConfig(), tabDeps)) - act(() => { result.current.handleOpenDailyNote() }) - expect(addEntry).toHaveBeenCalledTimes(1) - expect(addEntry.mock.calls[0][0].isA).toBe('Journal') - }) - - it('handleOpenDailyNote opens existing daily note', () => { - const today = todayDateString() - const existing = makeEntry({ filename: `${today}.md`, isA: 'Journal' }) - const { result } = renderHook(() => useNoteCreation(makeConfig([existing]), tabDeps)) - act(() => { result.current.handleOpenDailyNote() }) - expect(addEntry).not.toHaveBeenCalled() - expect(handleSelectNote).toHaveBeenCalledWith(existing) - }) - - it('handleOpenDailyNote requests focus for the daily note path', () => { - const focusListener = vi.fn() - window.addEventListener('laputa:focus-editor', focusListener) - const today = todayDateString() - const { result } = renderHook(() => useNoteCreation(makeConfig(), tabDeps)) - - act(() => { result.current.handleOpenDailyNote() }) - - expect(focusListener).toHaveBeenCalledTimes(1) - const event = focusListener.mock.calls[0][0] as CustomEvent - expect(event.detail.path).toBe(`/test/vault/${today}.md`) - - window.removeEventListener('laputa:focus-editor', focusListener) - }) - it('handleCreateType creates type entry', () => { const { result } = renderHook(() => useNoteCreation(makeConfig(), tabDeps)) act(() => { result.current.handleCreateType('Recipe') }) diff --git a/src/hooks/useNoteCreation.ts b/src/hooks/useNoteCreation.ts index e92657e7..16ead6e3 100644 --- a/src/hooks/useNoteCreation.ts +++ b/src/hooks/useNoteCreation.ts @@ -134,24 +134,6 @@ export function resolveNewType({ typeName, vaultPath }: NewTypeParams): { entry: return { entry, content: `---\ntype: Type\n---\n` } } -export function todayDateString(): string { - return new Date().toISOString().split('T')[0] -} - -export function buildDailyNoteContent(date: string): string { - const lines = ['---', `title: ${date}`, 'type: Journal', `date: ${date}`, '---'] - return `${lines.join('\n')}\n\n## Intentions\n\n\n\n## Reflections\n\n` -} - -export function resolveDailyNote(date: string, vaultPath: string): { entry: VaultEntry; content: string } { - const entry = buildNewEntry({ path: `${vaultPath}/${date}.md`, slug: date, title: date, type: 'Journal', status: null }) - return { entry, content: buildDailyNoteContent(date) } -} - -export function findDailyNote(entries: VaultEntry[], date: string): VaultEntry | undefined { - return entries.find(e => e.filename === `${date}.md` && e.isA === 'Journal') -} - /** Persist a newly created note to disk. Returns a Promise for error handling. */ export function persistNewNote(path: string, content: string): Promise { if (!isTauri()) return Promise.resolve() @@ -190,11 +172,12 @@ function persistOptimistic(path: string, content: string, cbs: PersistCallbacks) .catch(() => { cbs.onEnd?.(path); cbs.onFail(path) }) } -type PersistFn = (resolved: { entry: VaultEntry; content: string }) => void +type ResolvedNote = { entry: VaultEntry; content: string } +type PersistFn = (resolved: ResolvedNote) => void /** Optimistically open note, add entry to vault, and persist to disk. */ function createAndPersist( - resolved: { entry: VaultEntry; content: string }, + resolved: ResolvedNote, addFn: (e: VaultEntry) => void, openTab: (e: VaultEntry, c: string) => void, cbs: PersistCallbacks, @@ -204,16 +187,6 @@ function createAndPersist( persistOptimistic(resolved.entry.path, resolved.content, cbs) } -/** Open today's daily note: navigate to it if it exists, or create + persist a new one. */ -function openDailyNote(entries: VaultEntry[], selectNote: (e: VaultEntry) => void, persist: PersistFn, vaultPath: string): void { - const date = todayDateString() - const existing = findDailyNote(entries, date) - const targetPath = existing?.path ?? `${vaultPath}/${date}.md` - if (existing) selectNote(existing) - else persist(resolveDailyNote(date, vaultPath)) - signalFocusEditor({ path: targetPath }) -} - interface ImmediateCreateDeps { entries: VaultEntry[] vaultPath: string @@ -228,6 +201,15 @@ interface ImmediateCreateRequest { type?: string } +interface ImmediateCreateQueueConfig { + entries: VaultEntry[] + vaultPath: string + addEntry: (entry: VaultEntry) => void + openTabWithContent: (entry: VaultEntry, content: string) => void + trackUnsaved?: (path: string) => void + markContentPending?: (path: string, content: string) => void +} + /** Generate a unique untitled filename using a timestamp. */ function generateUntitledFilename(entries: VaultEntry[], type: string, pendingSlugs?: Set): string { const ts = Math.floor(Date.now() / 1000) @@ -262,6 +244,82 @@ function createNoteImmediate(deps: ImmediateCreateDeps, type?: string): void { signalFocusEditor({ path: entry.path, selectTitle: true }) } +function useImmediateCreateQueue(config: ImmediateCreateQueueConfig): (type?: string) => void { + const pendingSlugsRef = useRef>(new Set()) + const queuedImmediateCreatesRef = useRef([]) + const immediateCreateLockedRef = useRef(false) + const immediateCreateTimerRef = useRef(null) + const latestDepsRef = useRef(null) + + const syncDeps = useCallback(() => { + latestDepsRef.current = { + entries: config.entries, + vaultPath: config.vaultPath, + pendingSlugs: pendingSlugsRef.current, + openTabWithContent: config.openTabWithContent, + addEntry: config.addEntry, + trackUnsaved: config.trackUnsaved, + markContentPending: config.markContentPending, + } + }, [ + config.entries, + config.vaultPath, + config.openTabWithContent, + config.addEntry, + config.trackUnsaved, + config.markContentPending, + ]) + + useEffect(() => { + syncDeps() + }, [syncDeps]) + + const executeRequest = useCallback((request: ImmediateCreateRequest) => { + const deps = latestDepsRef.current + if (!deps) return + createNoteImmediate(deps, request.type) + trackEvent('note_created', { + has_type: request.type ? 1 : 0, + creation_path: request.type ? 'type_section' : 'cmd_n', + }) + }, []) + + const scheduleQueuedBurst = useCallback(function scheduleQueuedBurst() { + if (immediateCreateTimerRef.current !== null) return + + immediateCreateTimerRef.current = window.setTimeout(() => { + immediateCreateTimerRef.current = null + const next = queuedImmediateCreatesRef.current.shift() + if (!next) { + immediateCreateLockedRef.current = false + return + } + + executeRequest(next) + scheduleQueuedBurst() + }, RAPID_CREATE_NOTE_SETTLE_MS) + }, [executeRequest]) + + useEffect(() => () => { + if (immediateCreateTimerRef.current !== null) { + window.clearTimeout(immediateCreateTimerRef.current) + } + }, []) + + return useCallback((type?: string) => { + syncDeps() + const request = { type } + if (immediateCreateLockedRef.current) { + queuedImmediateCreatesRef.current.push(request) + return + } + + immediateCreateLockedRef.current = true + executeRequest(request) + scheduleQueuedBurst() + }, [syncDeps, executeRequest, scheduleQueuedBurst]) +} + interface RelationshipCreateDeps { entries: VaultEntry[] vaultPath: string @@ -303,40 +361,17 @@ export interface NoteCreationConfig { interface CreationTabDeps { openTabWithContent: (entry: VaultEntry, content: string) => void - handleSelectNote: (entry: VaultEntry) => void } export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTabDeps) { const { addEntry, removeEntry, entries, setToastMessage, addPendingSave, removePendingSave } = config - const { openTabWithContent, handleSelectNote } = tabDeps + const { openTabWithContent } = tabDeps const revertOptimisticNote = useCallback((path: string) => { removeEntry(path) setToastMessage('Failed to create note — disk write error') }, [removeEntry, setToastMessage]) - const pendingSlugsRef = useRef>(new Set()) - const queuedImmediateCreatesRef = useRef([]) - const immediateCreateLockedRef = useRef(false) - const immediateCreateTimerRef = useRef(null) - const latestImmediateCreateDepsRef = useRef(null) - - const syncImmediateCreateDeps = useCallback(() => { - latestImmediateCreateDepsRef.current = { - entries, - vaultPath: config.vaultPath, - pendingSlugs: pendingSlugsRef.current, - openTabWithContent, - addEntry, - trackUnsaved: config.trackUnsaved, - markContentPending: config.markContentPending, - } - }, [entries, config.vaultPath, openTabWithContent, addEntry, config.trackUnsaved, config.markContentPending]) - - useEffect(() => { - syncImmediateCreateDeps() - }, [syncImmediateCreateDeps]) - const persistNew: PersistFn = useCallback( (resolved) => createAndPersist(resolved, addEntry, openTabWithContent, { onFail: revertOptimisticNote, @@ -347,54 +382,21 @@ export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTab [openTabWithContent, addEntry, revertOptimisticNote, addPendingSave, removePendingSave, config.onNewNotePersisted], ) + const handleCreateNoteImmediate = useImmediateCreateQueue({ + entries, + vaultPath: config.vaultPath, + addEntry, + openTabWithContent, + trackUnsaved: config.trackUnsaved, + markContentPending: config.markContentPending, + }) + const handleCreateNote = useCallback((title: string, type: string) => { const template = resolveTemplate({ entries, typeName: type }) persistNew(resolveNewNote({ title, type, vaultPath: config.vaultPath, template })) trackEvent('note_created', { has_type: type !== 'Note' ? 1 : 0, creation_path: 'plus_button' }) }, [entries, persistNew, config.vaultPath]) - const executeImmediateCreateRequest = useCallback((request: ImmediateCreateRequest) => { - const deps = latestImmediateCreateDepsRef.current - if (!deps) return - createNoteImmediate(deps, request.type) - trackEvent('note_created', { has_type: request.type ? 1 : 0, creation_path: request.type ? 'type_section' : 'cmd_n' }) - }, []) - - const continueImmediateCreateBurst = useCallback(function scheduleImmediateCreateBurst() { - if (immediateCreateTimerRef.current !== null) return - - immediateCreateTimerRef.current = window.setTimeout(() => { - immediateCreateTimerRef.current = null - const next = queuedImmediateCreatesRef.current.shift() - if (!next) { - immediateCreateLockedRef.current = false - return - } - - executeImmediateCreateRequest(next) - scheduleImmediateCreateBurst() - }, RAPID_CREATE_NOTE_SETTLE_MS) - }, [executeImmediateCreateRequest]) - - const handleCreateNoteImmediate = useCallback((type?: string) => { - syncImmediateCreateDeps() - const request = { type } - if (immediateCreateLockedRef.current) { - queuedImmediateCreatesRef.current.push(request) - return - } - - immediateCreateLockedRef.current = true - executeImmediateCreateRequest(request) - continueImmediateCreateBurst() - }, [syncImmediateCreateDeps, executeImmediateCreateRequest, continueImmediateCreateBurst]) - - useEffect(() => () => { - if (immediateCreateTimerRef.current !== null) { - window.clearTimeout(immediateCreateTimerRef.current) - } - }, []) - const handleCreateNoteForRelationship = useCallback((title: string): Promise => { createNoteForRelationship({ entries, vaultPath: config.vaultPath, openTabWithContent, addEntry, @@ -403,8 +405,6 @@ export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTab return Promise.resolve(true) }, [entries, openTabWithContent, addEntry, removeEntry, setToastMessage, config.vaultPath, config.onNewNotePersisted]) - const handleOpenDailyNote = useCallback(() => openDailyNote(entries, handleSelectNote, persistNew, config.vaultPath), [entries, handleSelectNote, persistNew, config.vaultPath]) - const handleCreateType = useCallback((typeName: string) => { persistNew(resolveNewType({ typeName, vaultPath: config.vaultPath })) trackEvent('type_created') @@ -422,7 +422,6 @@ export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTab handleCreateNote, handleCreateNoteImmediate, handleCreateNoteForRelationship, - handleOpenDailyNote, handleCreateType, createTypeEntrySilent, }