feat: reorganize menu bar with Go, Note, and Vault menus
Add missing command palette commands to menu bar and reorganize into logical groups: File, Edit, View, Go, Note, Vault, Window. New menus expose navigation filters, note actions, vault management, themes, and git operations. Context-sensitive items (archive, trash, diff, raw editor, commit, conflicts) are greyed out when not applicable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -303,8 +303,19 @@ fn batch_trash_notes(paths: Vec<String>) -> Result<usize, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_menu_state(app_handle: tauri::AppHandle, has_active_note: bool) -> Result<(), String> {
|
||||
fn update_menu_state(
|
||||
app_handle: tauri::AppHandle,
|
||||
has_active_note: bool,
|
||||
has_modified_files: Option<bool>,
|
||||
has_conflicts: Option<bool>,
|
||||
) -> Result<(), String> {
|
||||
menu::set_note_items_enabled(&app_handle, has_active_note);
|
||||
if let Some(v) = has_modified_files {
|
||||
menu::set_git_commit_items_enabled(&app_handle, v);
|
||||
}
|
||||
if let Some(v) = has_conflicts {
|
||||
menu::set_git_conflict_items_enabled(&app_handle, v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,51 +5,109 @@ use tauri::{
|
||||
|
||||
// Custom menu item IDs that emit events to the frontend.
|
||||
const APP_SETTINGS: &str = "app-settings";
|
||||
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";
|
||||
const FILE_CLOSE_TAB: &str = "file-close-tab";
|
||||
|
||||
const EDIT_FIND_IN_VAULT: &str = "edit-find-in-vault";
|
||||
const EDIT_TOGGLE_RAW_EDITOR: &str = "edit-toggle-raw-editor";
|
||||
const EDIT_TOGGLE_DIFF: &str = "edit-toggle-diff";
|
||||
|
||||
const VIEW_EDITOR_ONLY: &str = "view-editor-only";
|
||||
const VIEW_EDITOR_LIST: &str = "view-editor-list";
|
||||
const VIEW_ALL: &str = "view-all";
|
||||
const VIEW_TOGGLE_INSPECTOR: &str = "view-toggle-inspector";
|
||||
const VIEW_TOGGLE_PROPERTIES: &str = "view-toggle-properties";
|
||||
const VIEW_TOGGLE_AI_CHAT: &str = "view-toggle-ai-chat";
|
||||
const VIEW_TOGGLE_BACKLINKS: &str = "view-toggle-backlinks";
|
||||
const VIEW_COMMAND_PALETTE: &str = "view-command-palette";
|
||||
const VIEW_ZOOM_IN: &str = "view-zoom-in";
|
||||
const VIEW_ZOOM_OUT: &str = "view-zoom-out";
|
||||
const VIEW_ZOOM_RESET: &str = "view-zoom-reset";
|
||||
const NOTE_ARCHIVE: &str = "note-archive";
|
||||
const NOTE_TRASH: &str = "note-trash";
|
||||
const EDIT_FIND_IN_VAULT: &str = "edit-find-in-vault";
|
||||
const VIEW_GO_BACK: &str = "view-go-back";
|
||||
const VIEW_GO_FORWARD: &str = "view-go-forward";
|
||||
const APP_CHECK_FOR_UPDATES: &str = "app-check-for-updates";
|
||||
|
||||
const GO_ALL_NOTES: &str = "go-all-notes";
|
||||
const GO_FAVORITES: &str = "go-favorites";
|
||||
const GO_ARCHIVED: &str = "go-archived";
|
||||
const GO_TRASH: &str = "go-trash";
|
||||
const GO_CHANGES: &str = "go-changes";
|
||||
|
||||
const NOTE_ARCHIVE: &str = "note-archive";
|
||||
const NOTE_TRASH: &str = "note-trash";
|
||||
|
||||
const VAULT_OPEN: &str = "vault-open";
|
||||
const VAULT_REMOVE: &str = "vault-remove";
|
||||
const VAULT_RESTORE_GETTING_STARTED: &str = "vault-restore-getting-started";
|
||||
const VAULT_NEW_THEME: &str = "vault-new-theme";
|
||||
const VAULT_RESTORE_DEFAULT_THEMES: &str = "vault-restore-default-themes";
|
||||
const VAULT_COMMIT_PUSH: &str = "vault-commit-push";
|
||||
const VAULT_RESOLVE_CONFLICTS: &str = "vault-resolve-conflicts";
|
||||
const VAULT_VIEW_CHANGES: &str = "vault-view-changes";
|
||||
const VAULT_INSTALL_MCP: &str = "vault-install-mcp";
|
||||
|
||||
const CUSTOM_IDS: &[&str] = &[
|
||||
APP_SETTINGS,
|
||||
APP_CHECK_FOR_UPDATES,
|
||||
FILE_NEW_NOTE,
|
||||
FILE_NEW_TYPE,
|
||||
FILE_DAILY_NOTE,
|
||||
FILE_QUICK_OPEN,
|
||||
FILE_SAVE,
|
||||
FILE_CLOSE_TAB,
|
||||
NOTE_ARCHIVE,
|
||||
NOTE_TRASH,
|
||||
EDIT_FIND_IN_VAULT,
|
||||
EDIT_TOGGLE_RAW_EDITOR,
|
||||
EDIT_TOGGLE_DIFF,
|
||||
VIEW_EDITOR_ONLY,
|
||||
VIEW_EDITOR_LIST,
|
||||
VIEW_ALL,
|
||||
VIEW_TOGGLE_INSPECTOR,
|
||||
VIEW_TOGGLE_PROPERTIES,
|
||||
VIEW_TOGGLE_AI_CHAT,
|
||||
VIEW_TOGGLE_BACKLINKS,
|
||||
VIEW_COMMAND_PALETTE,
|
||||
VIEW_ZOOM_IN,
|
||||
VIEW_ZOOM_OUT,
|
||||
VIEW_ZOOM_RESET,
|
||||
VIEW_GO_BACK,
|
||||
VIEW_GO_FORWARD,
|
||||
APP_CHECK_FOR_UPDATES,
|
||||
GO_ALL_NOTES,
|
||||
GO_FAVORITES,
|
||||
GO_ARCHIVED,
|
||||
GO_TRASH,
|
||||
GO_CHANGES,
|
||||
NOTE_ARCHIVE,
|
||||
NOTE_TRASH,
|
||||
VAULT_OPEN,
|
||||
VAULT_REMOVE,
|
||||
VAULT_RESTORE_GETTING_STARTED,
|
||||
VAULT_NEW_THEME,
|
||||
VAULT_RESTORE_DEFAULT_THEMES,
|
||||
VAULT_COMMIT_PUSH,
|
||||
VAULT_RESOLVE_CONFLICTS,
|
||||
VAULT_VIEW_CHANGES,
|
||||
VAULT_INSTALL_MCP,
|
||||
];
|
||||
|
||||
/// IDs of menu items that should be disabled when no note tab is active.
|
||||
const NOTE_DEPENDENT_IDS: &[&str] = &[FILE_SAVE, FILE_CLOSE_TAB, NOTE_ARCHIVE, NOTE_TRASH];
|
||||
const NOTE_DEPENDENT_IDS: &[&str] = &[
|
||||
FILE_SAVE,
|
||||
FILE_CLOSE_TAB,
|
||||
NOTE_ARCHIVE,
|
||||
NOTE_TRASH,
|
||||
EDIT_TOGGLE_RAW_EDITOR,
|
||||
EDIT_TOGGLE_DIFF,
|
||||
VIEW_TOGGLE_BACKLINKS,
|
||||
];
|
||||
|
||||
/// IDs of menu items that depend on having uncommitted changes.
|
||||
const GIT_COMMIT_DEPENDENT_IDS: &[&str] = &[VAULT_COMMIT_PUSH];
|
||||
|
||||
/// IDs of menu items that depend on having merge conflicts.
|
||||
const GIT_CONFLICT_DEPENDENT_IDS: &[&str] = &[VAULT_RESOLVE_CONFLICTS];
|
||||
|
||||
type MenuResult = Result<Submenu<tauri::Wry>, Box<dyn std::error::Error>>;
|
||||
|
||||
@@ -84,6 +142,9 @@ fn build_file_menu(app: &App) -> MenuResult {
|
||||
.id(FILE_NEW_NOTE)
|
||||
.accelerator("CmdOrCtrl+N")
|
||||
.build(app)?;
|
||||
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")
|
||||
@@ -100,25 +161,14 @@ fn build_file_menu(app: &App) -> MenuResult {
|
||||
.id(FILE_CLOSE_TAB)
|
||||
.accelerator("CmdOrCtrl+W")
|
||||
.build(app)?;
|
||||
let archive_note = MenuItemBuilder::new("Archive Note")
|
||||
.id(NOTE_ARCHIVE)
|
||||
.accelerator("CmdOrCtrl+E")
|
||||
.build(app)?;
|
||||
let trash_note = MenuItemBuilder::new("Trash Note")
|
||||
.id(NOTE_TRASH)
|
||||
.accelerator("CmdOrCtrl+Backspace")
|
||||
.build(app)?;
|
||||
|
||||
Ok(SubmenuBuilder::new(app, "File")
|
||||
.item(&new_note)
|
||||
.item(&new_type)
|
||||
.item(&daily_note)
|
||||
.item(&quick_open)
|
||||
.separator()
|
||||
.item(&save)
|
||||
.separator()
|
||||
.item(&archive_note)
|
||||
.item(&trash_note)
|
||||
.separator()
|
||||
.item(&close_tab)
|
||||
.build()?)
|
||||
}
|
||||
@@ -128,6 +178,13 @@ fn build_edit_menu(app: &App) -> MenuResult {
|
||||
.id(EDIT_FIND_IN_VAULT)
|
||||
.accelerator("CmdOrCtrl+Shift+F")
|
||||
.build(app)?;
|
||||
let toggle_raw_editor = MenuItemBuilder::new("Toggle Raw Editor")
|
||||
.id(EDIT_TOGGLE_RAW_EDITOR)
|
||||
.accelerator("CmdOrCtrl+\\")
|
||||
.build(app)?;
|
||||
let toggle_diff = MenuItemBuilder::new("Toggle Diff Mode")
|
||||
.id(EDIT_TOGGLE_DIFF)
|
||||
.build(app)?;
|
||||
|
||||
Ok(SubmenuBuilder::new(app, "Edit")
|
||||
.undo()
|
||||
@@ -140,6 +197,8 @@ fn build_edit_menu(app: &App) -> MenuResult {
|
||||
.select_all()
|
||||
.separator()
|
||||
.item(&find_in_vault)
|
||||
.item(&toggle_raw_editor)
|
||||
.item(&toggle_diff)
|
||||
.build()?)
|
||||
}
|
||||
|
||||
@@ -156,8 +215,15 @@ fn build_view_menu(app: &App) -> MenuResult {
|
||||
.id(VIEW_ALL)
|
||||
.accelerator("CmdOrCtrl+3")
|
||||
.build(app)?;
|
||||
let toggle_inspector = MenuItemBuilder::new("Toggle Inspector")
|
||||
.id(VIEW_TOGGLE_INSPECTOR)
|
||||
let toggle_properties = MenuItemBuilder::new("Toggle Properties Panel")
|
||||
.id(VIEW_TOGGLE_PROPERTIES)
|
||||
.build(app)?;
|
||||
let toggle_ai_chat = MenuItemBuilder::new("Toggle AI Chat")
|
||||
.id(VIEW_TOGGLE_AI_CHAT)
|
||||
.accelerator("CmdOrCtrl+I")
|
||||
.build(app)?;
|
||||
let toggle_backlinks = MenuItemBuilder::new("Toggle Backlinks")
|
||||
.id(VIEW_TOGGLE_BACKLINKS)
|
||||
.build(app)?;
|
||||
let command_palette = MenuItemBuilder::new("Command Palette")
|
||||
.id(VIEW_COMMAND_PALETTE)
|
||||
@@ -189,19 +255,109 @@ fn build_view_menu(app: &App) -> MenuResult {
|
||||
.item(&editor_list)
|
||||
.item(&all_panels)
|
||||
.separator()
|
||||
.item(&toggle_inspector)
|
||||
.separator()
|
||||
.item(&go_back)
|
||||
.item(&go_forward)
|
||||
.item(&toggle_properties)
|
||||
.item(&toggle_ai_chat)
|
||||
.item(&toggle_backlinks)
|
||||
.separator()
|
||||
.item(&zoom_in)
|
||||
.item(&zoom_out)
|
||||
.item(&zoom_reset)
|
||||
.separator()
|
||||
.item(&go_back)
|
||||
.item(&go_forward)
|
||||
.separator()
|
||||
.item(&command_palette)
|
||||
.build()?)
|
||||
}
|
||||
|
||||
fn build_go_menu(app: &App) -> MenuResult {
|
||||
let all_notes = MenuItemBuilder::new("All Notes")
|
||||
.id(GO_ALL_NOTES)
|
||||
.build(app)?;
|
||||
let favorites = MenuItemBuilder::new("Favorites")
|
||||
.id(GO_FAVORITES)
|
||||
.build(app)?;
|
||||
let archived = MenuItemBuilder::new("Archived")
|
||||
.id(GO_ARCHIVED)
|
||||
.build(app)?;
|
||||
let trash = MenuItemBuilder::new("Trash")
|
||||
.id(GO_TRASH)
|
||||
.build(app)?;
|
||||
let changes = MenuItemBuilder::new("Changes")
|
||||
.id(GO_CHANGES)
|
||||
.build(app)?;
|
||||
|
||||
Ok(SubmenuBuilder::new(app, "Go")
|
||||
.item(&all_notes)
|
||||
.item(&favorites)
|
||||
.item(&archived)
|
||||
.item(&trash)
|
||||
.item(&changes)
|
||||
.build()?)
|
||||
}
|
||||
|
||||
fn build_note_menu(app: &App) -> MenuResult {
|
||||
let archive_note = MenuItemBuilder::new("Archive Note")
|
||||
.id(NOTE_ARCHIVE)
|
||||
.accelerator("CmdOrCtrl+E")
|
||||
.build(app)?;
|
||||
let trash_note = MenuItemBuilder::new("Trash Note")
|
||||
.id(NOTE_TRASH)
|
||||
.accelerator("CmdOrCtrl+Backspace")
|
||||
.build(app)?;
|
||||
|
||||
Ok(SubmenuBuilder::new(app, "Note")
|
||||
.item(&archive_note)
|
||||
.item(&trash_note)
|
||||
.build()?)
|
||||
}
|
||||
|
||||
fn build_vault_menu(app: &App) -> MenuResult {
|
||||
let open_vault = MenuItemBuilder::new("Open Vault…")
|
||||
.id(VAULT_OPEN)
|
||||
.build(app)?;
|
||||
let remove_vault = MenuItemBuilder::new("Remove Vault from List")
|
||||
.id(VAULT_REMOVE)
|
||||
.build(app)?;
|
||||
let restore_getting_started = MenuItemBuilder::new("Restore Getting Started")
|
||||
.id(VAULT_RESTORE_GETTING_STARTED)
|
||||
.build(app)?;
|
||||
let new_theme = MenuItemBuilder::new("New Theme")
|
||||
.id(VAULT_NEW_THEME)
|
||||
.build(app)?;
|
||||
let restore_default_themes = MenuItemBuilder::new("Restore Default Themes")
|
||||
.id(VAULT_RESTORE_DEFAULT_THEMES)
|
||||
.build(app)?;
|
||||
let commit_push = MenuItemBuilder::new("Commit & Push")
|
||||
.id(VAULT_COMMIT_PUSH)
|
||||
.build(app)?;
|
||||
let resolve_conflicts = MenuItemBuilder::new("Resolve Conflicts")
|
||||
.id(VAULT_RESOLVE_CONFLICTS)
|
||||
.enabled(false)
|
||||
.build(app)?;
|
||||
let view_changes = MenuItemBuilder::new("View Pending Changes")
|
||||
.id(VAULT_VIEW_CHANGES)
|
||||
.build(app)?;
|
||||
let install_mcp = MenuItemBuilder::new("Install MCP Server")
|
||||
.id(VAULT_INSTALL_MCP)
|
||||
.build(app)?;
|
||||
|
||||
Ok(SubmenuBuilder::new(app, "Vault")
|
||||
.item(&open_vault)
|
||||
.item(&remove_vault)
|
||||
.item(&restore_getting_started)
|
||||
.separator()
|
||||
.item(&new_theme)
|
||||
.item(&restore_default_themes)
|
||||
.separator()
|
||||
.item(&commit_push)
|
||||
.item(&resolve_conflicts)
|
||||
.item(&view_changes)
|
||||
.separator()
|
||||
.item(&install_mcp)
|
||||
.build()?)
|
||||
}
|
||||
|
||||
fn build_window_menu(app: &App) -> MenuResult {
|
||||
Ok(SubmenuBuilder::new(app, "Window")
|
||||
.minimize()
|
||||
@@ -216,6 +372,9 @@ pub fn setup_menu(app: &App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file_menu = build_file_menu(app)?;
|
||||
let edit_menu = build_edit_menu(app)?;
|
||||
let view_menu = build_view_menu(app)?;
|
||||
let go_menu = build_go_menu(app)?;
|
||||
let note_menu = build_note_menu(app)?;
|
||||
let vault_menu = build_vault_menu(app)?;
|
||||
let window_menu = build_window_menu(app)?;
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
@@ -223,6 +382,9 @@ pub fn setup_menu(app: &App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
.item(&file_menu)
|
||||
.item(&edit_menu)
|
||||
.item(&view_menu)
|
||||
.item(&go_menu)
|
||||
.item(¬e_menu)
|
||||
.item(&vault_menu)
|
||||
.item(&window_menu)
|
||||
.build()?;
|
||||
|
||||
@@ -238,45 +400,78 @@ pub fn setup_menu(app: &App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on having an active note tab.
|
||||
pub fn set_note_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
fn set_items_enabled(app_handle: &AppHandle, ids: &[&str], enabled: bool) {
|
||||
let Some(menu) = app_handle.menu() else {
|
||||
return;
|
||||
};
|
||||
for id in NOTE_DEPENDENT_IDS {
|
||||
for id in ids {
|
||||
if let Some(MenuItemKind::MenuItem(mi)) = menu.get(*id) {
|
||||
let _ = mi.set_enabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on having an active note tab.
|
||||
pub fn set_note_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_items_enabled(app_handle, NOTE_DEPENDENT_IDS, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on having uncommitted changes.
|
||||
pub fn set_git_commit_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_items_enabled(app_handle, GIT_COMMIT_DEPENDENT_IDS, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on having merge conflicts.
|
||||
pub fn set_git_conflict_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_items_enabled(app_handle, GIT_CONFLICT_DEPENDENT_IDS, enabled);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn custom_ids_include_all_expected_items() {
|
||||
fn custom_ids_include_all_constants() {
|
||||
let expected = [
|
||||
"app-settings",
|
||||
"file-new-note",
|
||||
"file-daily-note",
|
||||
"file-quick-open",
|
||||
"file-save",
|
||||
"file-close-tab",
|
||||
"note-archive",
|
||||
"note-trash",
|
||||
"edit-find-in-vault",
|
||||
"view-editor-only",
|
||||
"view-editor-list",
|
||||
"view-all",
|
||||
"view-toggle-inspector",
|
||||
"view-command-palette",
|
||||
"view-zoom-in",
|
||||
"view-zoom-out",
|
||||
"view-zoom-reset",
|
||||
"view-go-back",
|
||||
"view-go-forward",
|
||||
"app-check-for-updates",
|
||||
APP_SETTINGS,
|
||||
APP_CHECK_FOR_UPDATES,
|
||||
FILE_NEW_NOTE,
|
||||
FILE_NEW_TYPE,
|
||||
FILE_DAILY_NOTE,
|
||||
FILE_QUICK_OPEN,
|
||||
FILE_SAVE,
|
||||
FILE_CLOSE_TAB,
|
||||
EDIT_FIND_IN_VAULT,
|
||||
EDIT_TOGGLE_RAW_EDITOR,
|
||||
EDIT_TOGGLE_DIFF,
|
||||
VIEW_EDITOR_ONLY,
|
||||
VIEW_EDITOR_LIST,
|
||||
VIEW_ALL,
|
||||
VIEW_TOGGLE_PROPERTIES,
|
||||
VIEW_TOGGLE_AI_CHAT,
|
||||
VIEW_TOGGLE_BACKLINKS,
|
||||
VIEW_COMMAND_PALETTE,
|
||||
VIEW_ZOOM_IN,
|
||||
VIEW_ZOOM_OUT,
|
||||
VIEW_ZOOM_RESET,
|
||||
VIEW_GO_BACK,
|
||||
VIEW_GO_FORWARD,
|
||||
GO_ALL_NOTES,
|
||||
GO_FAVORITES,
|
||||
GO_ARCHIVED,
|
||||
GO_TRASH,
|
||||
GO_CHANGES,
|
||||
NOTE_ARCHIVE,
|
||||
NOTE_TRASH,
|
||||
VAULT_OPEN,
|
||||
VAULT_REMOVE,
|
||||
VAULT_RESTORE_GETTING_STARTED,
|
||||
VAULT_NEW_THEME,
|
||||
VAULT_RESTORE_DEFAULT_THEMES,
|
||||
VAULT_COMMIT_PUSH,
|
||||
VAULT_RESOLVE_CONFLICTS,
|
||||
VAULT_VIEW_CHANGES,
|
||||
VAULT_INSTALL_MCP,
|
||||
];
|
||||
for id in &expected {
|
||||
assert!(CUSTOM_IDS.contains(id), "missing custom ID: {id}");
|
||||
@@ -292,4 +487,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_dependent_ids_are_subset_of_custom_ids() {
|
||||
for id in GIT_COMMIT_DEPENDENT_IDS {
|
||||
assert!(
|
||||
CUSTOM_IDS.contains(id),
|
||||
"git-commit-dependent ID {id} not in CUSTOM_IDS"
|
||||
);
|
||||
}
|
||||
for id in GIT_CONFLICT_DEPENDENT_IDS {
|
||||
assert!(
|
||||
CUSTOM_IDS.contains(id),
|
||||
"git-conflict-dependent ID {id} not in CUSTOM_IDS"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_custom_ids() {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for id in CUSTOM_IDS {
|
||||
assert!(seen.insert(id), "duplicate custom ID: {id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,16 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
;(entry?.trashed ? config.onRestoreNote : config.onTrashNote)(path)
|
||||
}, [config.onTrashNote, config.onRestoreNote])
|
||||
|
||||
const { onSelect } = config
|
||||
|
||||
const selectFilter = useCallback((filter: 'all' | 'favorites' | 'archived' | 'trash' | 'changes') => {
|
||||
onSelect({ kind: 'filter', filter })
|
||||
}, [onSelect])
|
||||
|
||||
const viewChanges = useCallback(() => {
|
||||
onSelect({ kind: 'filter', filter: 'changes' })
|
||||
}, [onSelect])
|
||||
|
||||
useAppKeyboard({
|
||||
onQuickOpen: config.onQuickOpen,
|
||||
onCommandPalette: config.onCommandPalette,
|
||||
@@ -110,6 +120,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
useMenuEvents({
|
||||
onSetViewMode: config.onSetViewMode,
|
||||
onCreateNote: config.onCreateNote,
|
||||
onCreateType: config.onCreateType,
|
||||
onOpenDailyNote: config.onOpenDailyNote,
|
||||
onQuickOpen: config.onQuickOpen,
|
||||
onSave: config.onSave,
|
||||
@@ -122,12 +133,27 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
onArchiveNote: toggleArchive,
|
||||
onTrashNote: toggleTrash,
|
||||
onSearch: config.onSearch,
|
||||
onToggleRawEditor: config.onToggleRawEditor,
|
||||
onToggleDiff: config.onToggleDiff,
|
||||
onToggleAIChat: config.onToggleAIChat,
|
||||
onGoBack: config.onGoBack,
|
||||
onGoForward: config.onGoForward,
|
||||
onCheckForUpdates: config.onCheckForUpdates,
|
||||
onSelectFilter: selectFilter,
|
||||
onOpenVault: config.onOpenVault,
|
||||
onRemoveActiveVault: config.onRemoveActiveVault,
|
||||
onRestoreGettingStarted: config.onRestoreGettingStarted,
|
||||
onCreateTheme: config.onCreateTheme,
|
||||
onRestoreDefaultThemes: config.onRestoreDefaultThemes,
|
||||
onCommitPush: config.onCommitPush,
|
||||
onResolveConflicts: config.onResolveConflicts,
|
||||
onViewChanges: viewChanges,
|
||||
onInstallMcp: config.onInstallMcp,
|
||||
activeTabPathRef: config.activeTabPathRef,
|
||||
handleCloseTabRef: config.handleCloseTabRef,
|
||||
activeTabPath: config.activeTabPath,
|
||||
modifiedCount: config.modifiedCount,
|
||||
conflictCount: config.conflictCount,
|
||||
})
|
||||
|
||||
const commands = useCommandRegistry({
|
||||
@@ -150,6 +176,8 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
onToggleInspector: config.onToggleInspector,
|
||||
onToggleDiff: config.onToggleDiff,
|
||||
onToggleRawEditor: config.onToggleRawEditor,
|
||||
onToggleAIChat: config.onToggleAIChat,
|
||||
onOpenVault: config.onOpenVault,
|
||||
activeNoteModified: config.activeNoteModified,
|
||||
onZoomIn: config.onZoomIn,
|
||||
onZoomOut: config.onZoomOut,
|
||||
@@ -167,10 +195,8 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
onSwitchTheme: config.onSwitchTheme,
|
||||
onCreateTheme: config.onCreateTheme,
|
||||
onOpenTheme: config.onOpenTheme,
|
||||
onOpenVault: config.onOpenVault,
|
||||
onCreateType: config.onCreateType,
|
||||
onToggleAIChat: config.onToggleAIChat,
|
||||
onCheckForUpdates: config.onCheckForUpdates,
|
||||
onCreateType: config.onCreateType,
|
||||
onRemoveActiveVault: config.onRemoveActiveVault,
|
||||
onRestoreGettingStarted: config.onRestoreGettingStarted,
|
||||
onRestoreDefaultThemes: config.onRestoreDefaultThemes,
|
||||
|
||||
@@ -5,6 +5,7 @@ function makeHandlers(): MenuEventHandlers {
|
||||
return {
|
||||
onSetViewMode: vi.fn(),
|
||||
onCreateNote: vi.fn(),
|
||||
onCreateType: vi.fn(),
|
||||
onOpenDailyNote: vi.fn(),
|
||||
onQuickOpen: vi.fn(),
|
||||
onSave: vi.fn(),
|
||||
@@ -17,9 +18,22 @@ function makeHandlers(): MenuEventHandlers {
|
||||
onArchiveNote: vi.fn(),
|
||||
onTrashNote: vi.fn(),
|
||||
onSearch: vi.fn(),
|
||||
onToggleRawEditor: vi.fn(),
|
||||
onToggleDiff: vi.fn(),
|
||||
onToggleAIChat: vi.fn(),
|
||||
onGoBack: vi.fn(),
|
||||
onGoForward: vi.fn(),
|
||||
onCheckForUpdates: vi.fn(),
|
||||
onSelectFilter: vi.fn(),
|
||||
onOpenVault: vi.fn(),
|
||||
onRemoveActiveVault: vi.fn(),
|
||||
onRestoreGettingStarted: vi.fn(),
|
||||
onCreateTheme: vi.fn(),
|
||||
onRestoreDefaultThemes: vi.fn(),
|
||||
onCommitPush: vi.fn(),
|
||||
onResolveConflicts: vi.fn(),
|
||||
onViewChanges: vi.fn(),
|
||||
onInstallMcp: vi.fn(),
|
||||
activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject<string | null>,
|
||||
handleCloseTabRef: { current: vi.fn() } as React.MutableRefObject<(path: string) => void>,
|
||||
activeTabPath: '/vault/test.md',
|
||||
@@ -27,6 +41,7 @@ function makeHandlers(): MenuEventHandlers {
|
||||
}
|
||||
|
||||
describe('dispatchMenuEvent', () => {
|
||||
// View mode events
|
||||
it('view-editor-only sets editor-only mode', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('view-editor-only', h)
|
||||
@@ -45,6 +60,7 @@ describe('dispatchMenuEvent', () => {
|
||||
expect(h.onSetViewMode).toHaveBeenCalledWith('all')
|
||||
})
|
||||
|
||||
// Simple handler events
|
||||
it('file-new-note triggers create note', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('file-new-note', h)
|
||||
@@ -88,9 +104,9 @@ describe('dispatchMenuEvent', () => {
|
||||
expect(h.onOpenSettings).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('view-toggle-inspector triggers toggle inspector', () => {
|
||||
it('view-toggle-properties triggers toggle inspector', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('view-toggle-inspector', h)
|
||||
dispatchMenuEvent('view-toggle-properties', h)
|
||||
expect(h.onToggleInspector).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -118,6 +134,13 @@ describe('dispatchMenuEvent', () => {
|
||||
expect(h.onZoomReset).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('edit-find-in-vault triggers search', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('edit-find-in-vault', h)
|
||||
expect(h.onSearch).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Active tab-dependent events
|
||||
it('note-archive triggers archive on active tab', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('note-archive', h)
|
||||
@@ -144,12 +167,7 @@ describe('dispatchMenuEvent', () => {
|
||||
expect(h.onTrashNote).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('edit-find-in-vault triggers search', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('edit-find-in-vault', h)
|
||||
expect(h.onSearch).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Optional handler events
|
||||
it('view-go-back triggers go back', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('view-go-back', h)
|
||||
@@ -168,6 +186,126 @@ describe('dispatchMenuEvent', () => {
|
||||
expect(h.onCheckForUpdates).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// New File menu items
|
||||
it('file-new-type triggers create type', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('file-new-type', h)
|
||||
expect(h.onCreateType).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// New Edit menu items
|
||||
it('edit-toggle-raw-editor triggers toggle raw editor', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('edit-toggle-raw-editor', h)
|
||||
expect(h.onToggleRawEditor).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('edit-toggle-diff triggers toggle diff', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('edit-toggle-diff', h)
|
||||
expect(h.onToggleDiff).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// New View menu items
|
||||
it('view-toggle-ai-chat triggers toggle AI chat', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('view-toggle-ai-chat', h)
|
||||
expect(h.onToggleAIChat).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('view-toggle-backlinks triggers toggle inspector', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('view-toggle-backlinks', h)
|
||||
expect(h.onToggleInspector).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Go menu events
|
||||
it('go-all-notes selects all filter', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('go-all-notes', h)
|
||||
expect(h.onSelectFilter).toHaveBeenCalledWith('all')
|
||||
})
|
||||
|
||||
it('go-favorites selects favorites filter', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('go-favorites', h)
|
||||
expect(h.onSelectFilter).toHaveBeenCalledWith('favorites')
|
||||
})
|
||||
|
||||
it('go-archived selects archived filter', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('go-archived', h)
|
||||
expect(h.onSelectFilter).toHaveBeenCalledWith('archived')
|
||||
})
|
||||
|
||||
it('go-trash selects trash filter', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('go-trash', h)
|
||||
expect(h.onSelectFilter).toHaveBeenCalledWith('trash')
|
||||
})
|
||||
|
||||
it('go-changes selects changes filter', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('go-changes', h)
|
||||
expect(h.onSelectFilter).toHaveBeenCalledWith('changes')
|
||||
})
|
||||
|
||||
// Vault menu events
|
||||
it('vault-open triggers open vault', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-open', h)
|
||||
expect(h.onOpenVault).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-remove triggers remove active vault', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-remove', h)
|
||||
expect(h.onRemoveActiveVault).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-restore-getting-started triggers restore', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-restore-getting-started', h)
|
||||
expect(h.onRestoreGettingStarted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-new-theme triggers create theme', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-new-theme', h)
|
||||
expect(h.onCreateTheme).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-restore-default-themes triggers restore default themes', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-restore-default-themes', h)
|
||||
expect(h.onRestoreDefaultThemes).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-commit-push triggers commit push', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-commit-push', h)
|
||||
expect(h.onCommitPush).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-resolve-conflicts triggers resolve conflicts', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-resolve-conflicts', h)
|
||||
expect(h.onResolveConflicts).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-view-changes triggers view changes', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-view-changes', h)
|
||||
expect(h.onViewChanges).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vault-install-mcp triggers install MCP', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('vault-install-mcp', h)
|
||||
expect(h.onInstallMcp).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
it('unknown event ID does nothing', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('unknown-event', h)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { isTauri } from '../mock-tauri'
|
||||
import type { SidebarFilter } from '../types'
|
||||
import type { ViewMode } from './useViewMode'
|
||||
|
||||
export interface MenuEventHandlers {
|
||||
onSetViewMode: (mode: ViewMode) => void
|
||||
onCreateNote: () => void
|
||||
onCreateType?: () => void
|
||||
onOpenDailyNote: () => void
|
||||
onQuickOpen: () => void
|
||||
onSave: () => void
|
||||
@@ -17,12 +19,27 @@ export interface MenuEventHandlers {
|
||||
onArchiveNote: (path: string) => void
|
||||
onTrashNote: (path: string) => void
|
||||
onSearch: () => void
|
||||
onToggleRawEditor?: () => void
|
||||
onToggleDiff?: () => void
|
||||
onToggleAIChat?: () => void
|
||||
onGoBack?: () => void
|
||||
onGoForward?: () => void
|
||||
onCheckForUpdates?: () => void
|
||||
onSelectFilter?: (filter: SidebarFilter) => void
|
||||
onOpenVault?: () => void
|
||||
onRemoveActiveVault?: () => void
|
||||
onRestoreGettingStarted?: () => void
|
||||
onCreateTheme?: () => void
|
||||
onRestoreDefaultThemes?: () => void
|
||||
onCommitPush?: () => void
|
||||
onResolveConflicts?: () => void
|
||||
onViewChanges?: () => void
|
||||
onInstallMcp?: () => void
|
||||
activeTabPathRef: React.MutableRefObject<string | null>
|
||||
handleCloseTabRef: React.MutableRefObject<(path: string) => void>
|
||||
activeTabPath: string | null
|
||||
modifiedCount?: number
|
||||
conflictCount?: number
|
||||
}
|
||||
|
||||
const VIEW_MODE_MAP: Record<string, ViewMode> = {
|
||||
@@ -39,7 +56,7 @@ const SIMPLE_EVENT_MAP: Record<string, SimpleHandler> = {
|
||||
'file-quick-open': 'onQuickOpen',
|
||||
'file-save': 'onSave',
|
||||
'app-settings': 'onOpenSettings',
|
||||
'view-toggle-inspector': 'onToggleInspector',
|
||||
'view-toggle-properties': 'onToggleInspector',
|
||||
'view-command-palette': 'onCommandPalette',
|
||||
'view-zoom-in': 'onZoomIn',
|
||||
'view-zoom-out': 'onZoomOut',
|
||||
@@ -47,6 +64,41 @@ const SIMPLE_EVENT_MAP: Record<string, SimpleHandler> = {
|
||||
'edit-find-in-vault': 'onSearch',
|
||||
}
|
||||
|
||||
const FILTER_MAP: Record<string, SidebarFilter> = {
|
||||
'go-all-notes': 'all',
|
||||
'go-favorites': 'favorites',
|
||||
'go-archived': 'archived',
|
||||
'go-trash': 'trash',
|
||||
'go-changes': 'changes',
|
||||
}
|
||||
|
||||
type OptionalHandler =
|
||||
| 'onGoBack' | 'onGoForward' | 'onCheckForUpdates'
|
||||
| 'onCreateType' | 'onToggleRawEditor' | 'onToggleDiff' | 'onToggleAIChat'
|
||||
| 'onOpenVault' | 'onRemoveActiveVault' | 'onRestoreGettingStarted'
|
||||
| 'onCreateTheme' | 'onRestoreDefaultThemes'
|
||||
| 'onCommitPush' | 'onResolveConflicts' | 'onViewChanges' | 'onInstallMcp'
|
||||
|
||||
const OPTIONAL_EVENT_MAP: Record<string, OptionalHandler> = {
|
||||
'view-go-back': 'onGoBack',
|
||||
'view-go-forward': 'onGoForward',
|
||||
'app-check-for-updates': 'onCheckForUpdates',
|
||||
'file-new-type': 'onCreateType',
|
||||
'edit-toggle-raw-editor': 'onToggleRawEditor',
|
||||
'edit-toggle-diff': 'onToggleDiff',
|
||||
'view-toggle-ai-chat': 'onToggleAIChat',
|
||||
'view-toggle-backlinks': 'onToggleInspector',
|
||||
'vault-open': 'onOpenVault',
|
||||
'vault-remove': 'onRemoveActiveVault',
|
||||
'vault-restore-getting-started': 'onRestoreGettingStarted',
|
||||
'vault-new-theme': 'onCreateTheme',
|
||||
'vault-restore-default-themes': 'onRestoreDefaultThemes',
|
||||
'vault-commit-push': 'onCommitPush',
|
||||
'vault-resolve-conflicts': 'onResolveConflicts',
|
||||
'vault-view-changes': 'onViewChanges',
|
||||
'vault-install-mcp': 'onInstallMcp',
|
||||
}
|
||||
|
||||
function dispatchActiveTabEvent(id: string, h: MenuEventHandlers): boolean {
|
||||
const path = h.activeTabPathRef.current
|
||||
if (!path) return id === 'note-archive' || id === 'note-trash' || id === 'file-close-tab'
|
||||
@@ -57,9 +109,14 @@ function dispatchActiveTabEvent(id: string, h: MenuEventHandlers): boolean {
|
||||
}
|
||||
|
||||
function dispatchOptionalEvent(id: string, h: MenuEventHandlers): boolean {
|
||||
if (id === 'view-go-back') { h.onGoBack?.(); return true }
|
||||
if (id === 'view-go-forward') { h.onGoForward?.(); return true }
|
||||
if (id === 'app-check-for-updates') { h.onCheckForUpdates?.(); return true }
|
||||
const handler = OPTIONAL_EVENT_MAP[id]
|
||||
if (handler) { h[handler]?.(); return true }
|
||||
return false
|
||||
}
|
||||
|
||||
function dispatchFilterEvent(id: string, h: MenuEventHandlers): boolean {
|
||||
const filter = FILTER_MAP[id]
|
||||
if (filter) { h.onSelectFilter?.(filter); return true }
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -72,6 +129,7 @@ export function dispatchMenuEvent(id: string, h: MenuEventHandlers): void {
|
||||
if (simple) { h[simple](); return }
|
||||
|
||||
if (dispatchActiveTabEvent(id, h)) return
|
||||
if (dispatchFilterEvent(id, h)) return
|
||||
dispatchOptionalEvent(id, h)
|
||||
}
|
||||
|
||||
@@ -95,11 +153,15 @@ export function useMenuEvents(handlers: MenuEventHandlers) {
|
||||
return () => cleanup?.()
|
||||
}, [])
|
||||
|
||||
// Sync menu item enabled state when active tab changes
|
||||
// Sync menu item enabled state when active tab or git state changes
|
||||
useEffect(() => {
|
||||
if (!isTauri()) return
|
||||
import('@tauri-apps/api/core').then(({ invoke }) => {
|
||||
invoke('update_menu_state', { hasActiveNote: handlers.activeTabPath !== null })
|
||||
invoke('update_menu_state', {
|
||||
hasActiveNote: handlers.activeTabPath !== null,
|
||||
hasModifiedFiles: handlers.modifiedCount != null ? handlers.modifiedCount > 0 : undefined,
|
||||
hasConflicts: handlers.conflictCount != null ? handlers.conflictCount > 0 : undefined,
|
||||
})
|
||||
}).catch(() => {})
|
||||
}, [handlers.activeTabPath])
|
||||
}, [handlers.activeTabPath, handlers.modifiedCount, handlers.conflictCount])
|
||||
}
|
||||
|
||||
@@ -139,8 +139,10 @@ export interface VaultSettings {
|
||||
theme: string | null
|
||||
}
|
||||
|
||||
export type SidebarFilter = 'all' | 'favorites' | 'archived' | 'trash' | 'changes'
|
||||
|
||||
export type SidebarSelection =
|
||||
| { kind: 'filter'; filter: 'all' | 'favorites' | 'archived' | 'trash' | 'changes' }
|
||||
| { kind: 'filter'; filter: SidebarFilter }
|
||||
| { kind: 'sectionGroup'; type: string }
|
||||
| { kind: 'entity'; entry: VaultEntry }
|
||||
| { kind: 'topic'; entry: VaultEntry }
|
||||
|
||||
Reference in New Issue
Block a user