diff --git a/src/renderer/lib/commands/register-menu-commands.ts b/src/renderer/lib/commands/register-menu-commands.ts index 1db22fe..e6e3f9c 100644 --- a/src/renderer/lib/commands/register-menu-commands.ts +++ b/src/renderer/lib/commands/register-menu-commands.ts @@ -12,41 +12,79 @@ import { useMenuAction } from '@/hooks/use-menu-action'; * Phase 7+ will add the dialog-driven commands (export, settings, etc.) * once the corresponding modals exist. */ +export function registerMenuCommands(): void { + const { registerMany } = useCommandStore.getState(); + + registerMany({ + 'settings.open': () => useAppStore.getState().openModal('settings'), + 'help.about': () => useAppStore.getState().openModal('about'), + 'help.welcome': () => useAppStore.getState().openModal('welcome'), + 'file.exportPdf': () => { + const activeTabId = useFileStore.getState().activeTabId; + if (!activeTabId) return; + useAppStore.getState().openModal('export-pdf', { sourcePath: activeTabId }); + }, + 'file.exportDocx': () => { + const activeTabId = useFileStore.getState().activeTabId; + if (!activeTabId) return; + useAppStore.getState().openModal('export-docx', { sourcePath: activeTabId }); + }, + 'file.exportHtml': () => { + const activeTabId = useFileStore.getState().activeTabId; + if (!activeTabId) return; + useAppStore.getState().openModal('export-html', { sourcePath: activeTabId }); + }, + 'file.exportBatch': () => { + const paths = useFileStore.getState().openTabs.map((t) => t.path); + if (paths.length === 0) return; + useAppStore.getState().openModal('export-batch', { sourcePaths: paths }); + }, + 'file.confirmClose': () => { + /* stub — wired in later phase */ + }, + 'app.quit': () => { + /* stub — wired in later phase */ + }, + }); + + const { register } = useCommandStore.getState(); + register('file.open', () => { + void useFileStore.getState().openFileDialog(); + }); + register('file.openFolder', () => { + void useFileStore.getState().openFolderDialog(); + }); + register('file.save', () => { + void useFileStore.getState().saveActiveBuffer(); + }); + register('file.closeTab', () => { + const { activeTabId, closeTab } = useFileStore.getState(); + if (activeTabId) closeTab(activeTabId); + }); + register('tab.next', () => { + const { openTabs, activeTabId, setActiveTab } = useFileStore.getState(); + if (openTabs.length === 0) return; + const idx = activeTabId ? openTabs.findIndex((t) => t.id === activeTabId) : 0; + const safeIdx = idx === -1 ? 0 : idx; + const nextIdx = safeIdx >= openTabs.length - 1 ? 0 : safeIdx + 1; + setActiveTab(openTabs[nextIdx].id); + }); + register('tab.prev', () => { + const { openTabs, activeTabId, setActiveTab } = useFileStore.getState(); + if (openTabs.length === 0) return; + const idx = activeTabId ? openTabs.findIndex((t) => t.id === activeTabId) : 0; + const safeIdx = idx === -1 ? 0 : idx; + const nextIdx = safeIdx <= 0 ? openTabs.length - 1 : safeIdx - 1; + setActiveTab(openTabs[nextIdx].id); + }); + register('view.toggleSidebar', () => useAppStore.getState().toggleSidebar()); + register('view.togglePreview', () => useAppStore.getState().togglePreview()); +} + export function useRegisterMenuCommands(): void { // Register handlers in the command store. useEffect(() => { - const { register } = useCommandStore.getState(); - register('file.open', () => { - void useFileStore.getState().openFileDialog(); - }); - register('file.openFolder', () => { - void useFileStore.getState().openFolderDialog(); - }); - register('file.save', () => { - void useFileStore.getState().saveActiveBuffer(); - }); - register('file.closeTab', () => { - const { activeTabId, closeTab } = useFileStore.getState(); - if (activeTabId) closeTab(activeTabId); - }); - register('tab.next', () => { - const { openTabs, activeTabId, setActiveTab } = useFileStore.getState(); - if (openTabs.length === 0) return; - const idx = activeTabId ? openTabs.findIndex((t) => t.id === activeTabId) : 0; - const safeIdx = idx === -1 ? 0 : idx; - const nextIdx = safeIdx >= openTabs.length - 1 ? 0 : safeIdx + 1; - setActiveTab(openTabs[nextIdx].id); - }); - register('tab.prev', () => { - const { openTabs, activeTabId, setActiveTab } = useFileStore.getState(); - if (openTabs.length === 0) return; - const idx = activeTabId ? openTabs.findIndex((t) => t.id === activeTabId) : 0; - const safeIdx = idx === -1 ? 0 : idx; - const nextIdx = safeIdx <= 0 ? openTabs.length - 1 : safeIdx - 1; - setActiveTab(openTabs[nextIdx].id); - }); - register('view.toggleSidebar', () => useAppStore.getState().toggleSidebar()); - register('view.togglePreview', () => useAppStore.getState().togglePreview()); + registerMenuCommands(); }, []); } diff --git a/tests/unit/commands/modal-commands.test.ts b/tests/unit/commands/modal-commands.test.ts new file mode 100644 index 0000000..1e6403a --- /dev/null +++ b/tests/unit/commands/modal-commands.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { registerMenuCommands } from '@/lib/commands/register-menu-commands'; +import { useCommandStore } from '@/stores/command-store'; +import { useAppStore } from '@/stores/app-store'; +import { useFileStore } from '@/stores/file-store'; +import { useEditorStore } from '@/stores/editor-store'; + +describe('modal commands', () => { + beforeEach(() => { + localStorage.clear(); + useCommandStore.setState({ handlers: {}, userBindings: {} } as any); + useAppStore.setState({ modal: { kind: null } } as any); + useFileStore.setState({ activeTabId: '/x.md', openTabs: [{ id: '/x.md', path: '/x.md', title: 'x.md', dirty: false }] } as any); + useEditorStore.setState({ buffers: new Map([['/x.md', { id: '/x.md', path: '/x.md', content: '# hi', dirty: false }]]) } as any); + }); + + it('settings.open opens settings modal', () => { + registerMenuCommands(); + useCommandStore.getState().dispatch('settings.open'); + expect(useAppStore.getState().modal).toEqual({ kind: 'settings' }); + }); + + it('help.about opens about modal', () => { + registerMenuCommands(); + useCommandStore.getState().dispatch('help.about'); + expect(useAppStore.getState().modal).toEqual({ kind: 'about' }); + }); + + it('help.welcome opens welcome modal', () => { + registerMenuCommands(); + useCommandStore.getState().dispatch('help.welcome'); + expect(useAppStore.getState().modal).toEqual({ kind: 'welcome' }); + }); + + it('file.exportPdf opens export-pdf modal with active path', () => { + registerMenuCommands(); + useCommandStore.getState().dispatch('file.exportPdf'); + expect(useAppStore.getState().modal).toEqual({ kind: 'export-pdf', props: { sourcePath: '/x.md' } }); + }); + + it('file.exportDocx opens export-docx modal', () => { + registerMenuCommands(); + useCommandStore.getState().dispatch('file.exportDocx'); + expect(useAppStore.getState().modal).toEqual({ kind: 'export-docx', props: { sourcePath: '/x.md' } }); + }); + + it('file.exportBatch opens export-batch modal with all open files', () => { + useFileStore.setState({ activeTabId: '/x.md', openTabs: [{ id: '/x.md', path: '/x.md', title: 'x.md', dirty: false }, { id: '/y.md', path: '/y.md', title: 'y.md', dirty: false }] } as any); + registerMenuCommands(); + useCommandStore.getState().dispatch('file.exportBatch'); + expect(useAppStore.getState().modal).toEqual({ kind: 'export-batch', props: { sourcePaths: ['/x.md', '/y.md'] } }); + }); +}); \ No newline at end of file