fix: remove daily note command surfaces
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AppCommandId, AppCommandDefinition>
|
||||
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<string>(
|
||||
const MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET = new Set<AppCommandId>([
|
||||
APP_COMMAND_IDS.appSettings,
|
||||
APP_COMMAND_IDS.fileNewNote,
|
||||
APP_COMMAND_IDS.fileDailyNote,
|
||||
APP_COMMAND_IDS.fileQuickOpen,
|
||||
APP_COMMAND_IDS.fileSave,
|
||||
APP_COMMAND_IDS.editFindInVault,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<SimpleHandlerKey, (handlers: AppCommandHa
|
||||
onCheckForUpdates: (handlers) => 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(),
|
||||
|
||||
@@ -12,7 +12,6 @@ export type KeyboardActions = Pick<
|
||||
| 'onCommandPalette'
|
||||
| 'onSearch'
|
||||
| 'onCreateNote'
|
||||
| 'onOpenDailyNote'
|
||||
| 'onSave'
|
||||
| 'onOpenSettings'
|
||||
| 'onDeleteNote'
|
||||
|
||||
@@ -5,7 +5,6 @@ interface NavigationCommandsConfig {
|
||||
onQuickOpen: () => void
|
||||
onSelect: (sel: SidebarSelection) => void
|
||||
showInbox?: boolean
|
||||
onOpenDailyNote: () => void
|
||||
onGoBack?: () => void
|
||||
onGoForward?: () => void
|
||||
canGoBack?: boolean
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<Parameters<typeof useAppKeyboard>[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<Parameters<typeof useMenuEvents>[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<typeof useCommandRegistry>[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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -31,7 +31,6 @@ function makeConfig(overrides: Record<string, unknown> = {}) {
|
||||
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 })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<void> {
|
||||
interface RenameAfterTitleChangeParams {
|
||||
path: string
|
||||
newTitle: string
|
||||
deps: TitleRenameDeps
|
||||
}
|
||||
|
||||
async function renameAfterTitleChange({ path, newTitle, deps }: RenameAfterTitleChangeParams): Promise<void> {
|
||||
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<void> {
|
||||
interface MaybeRenameAfterFrontmatterUpdateParams {
|
||||
path: string
|
||||
key: string
|
||||
value: FrontmatterValue
|
||||
deps: TitleRenameDeps
|
||||
}
|
||||
|
||||
async function maybeRenameAfterFrontmatterUpdate({
|
||||
path,
|
||||
key,
|
||||
value,
|
||||
deps,
|
||||
}: MaybeRenameAfterFrontmatterUpdateParams): Promise<void> {
|
||||
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,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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') })
|
||||
|
||||
@@ -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<void> {
|
||||
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>): 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<Set<string>>(new Set())
|
||||
const queuedImmediateCreatesRef = useRef<ImmediateCreateRequest[]>([])
|
||||
const immediateCreateLockedRef = useRef(false)
|
||||
const immediateCreateTimerRef = useRef<number | null>(null)
|
||||
const latestDepsRef = useRef<ImmediateCreateDeps | null>(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<Set<string>>(new Set())
|
||||
const queuedImmediateCreatesRef = useRef<ImmediateCreateRequest[]>([])
|
||||
const immediateCreateLockedRef = useRef(false)
|
||||
const immediateCreateTimerRef = useRef<number | null>(null)
|
||||
const latestImmediateCreateDepsRef = useRef<ImmediateCreateDeps | null>(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<boolean> => {
|
||||
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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user