Merge pull request #18 from refactoringhq/task/vault-picker-local
fix: vault picker — aggiungi opzione crea vault locale
This commit is contained in:
22
.claude-done
22
.claude-done
@@ -1,19 +1,3 @@
|
||||
## editor-save-bug — Done
|
||||
|
||||
### What was changed
|
||||
1. **Removed broken auto-save**: Deleted `useAutoSave` hook (debounced save) which was causing the editor to reload previous content on every keystroke.
|
||||
2. **Added explicit Cmd+S save**: New `useSaveNote` + `useEditorSave` hooks. Editor onChange buffers content in a ref; Cmd+S persists to disk. Shows "Saved" toast on success, "Nothing to save" when clean.
|
||||
3. **Fixed rename-before-save**: `handleRenameTab` now calls `savePendingForPath(path)` before executing the rename, ensuring the file on disk is up to date. No more "Failed to rename note" error.
|
||||
|
||||
### Architecture decisions
|
||||
- **No auto-save by design**: Consistent with git-based UX. User explicitly saves with Cmd+S, then commits when ready.
|
||||
- **Pending content tracked via ref** (not state): Avoids unnecessary re-renders on every keystroke. The ref holds `{ path, content }` set by Editor onChange, read by handleSave.
|
||||
- **useEditorSave extracted from App**: Reduces App component cyclomatic complexity (CodeScene quality gate passed).
|
||||
|
||||
### Tests
|
||||
- 471 unit tests passing (was 466 before)
|
||||
- New: `useSaveNote.test.ts` (3 tests), `useEditorSave.test.ts` (5 tests)
|
||||
- Updated: `App.test.tsx` (Cmd+S now shows "Nothing to save" when no pending content)
|
||||
- E2E: 2 tests passing (editor loads + Cmd+S shortcut works)
|
||||
- Coverage: 78.82% (above 70% threshold)
|
||||
- CodeScene quality gates: passed
|
||||
Task: vault-picker-local
|
||||
Summary: Added local vault creation options to vault picker
|
||||
Commits: 5
|
||||
|
||||
14536
design/vault-picker-local.pen
Normal file
14536
design/vault-picker-local.pen
Normal file
File diff suppressed because it is too large
Load Diff
39
e2e/vault-picker-local.spec.ts
Normal file
39
e2e/vault-picker-local.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Vault Picker Local Options', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://localhost:5240')
|
||||
await page.waitForSelector('text=notes', { timeout: 10000 })
|
||||
})
|
||||
|
||||
test('vault menu shows local folder options', async ({ page }) => {
|
||||
// Screenshot before opening menu
|
||||
await page.screenshot({ path: 'test-results/vault-picker-before.png', fullPage: true })
|
||||
|
||||
// Click the vault button in the status bar to open the menu
|
||||
const vaultButton = page.locator('[title="Switch vault"]')
|
||||
await expect(vaultButton).toBeVisible()
|
||||
await vaultButton.click()
|
||||
|
||||
// Wait for menu to appear
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Verify all three options are visible
|
||||
await expect(page.locator('text=Open local folder')).toBeVisible()
|
||||
await expect(page.locator('text=Create new vault')).toBeVisible()
|
||||
await expect(page.locator('text=Connect GitHub repo')).toBeVisible()
|
||||
|
||||
// Screenshot with menu open showing all options
|
||||
await page.screenshot({ path: 'test-results/vault-picker-menu-open.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('vault menu options have correct test IDs', async ({ page }) => {
|
||||
const vaultButton = page.locator('[title="Switch vault"]')
|
||||
await vaultButton.click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('[data-testid="vault-menu-open-local"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="vault-menu-create-new"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="vault-menu-connect-github"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -33,6 +33,7 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -62,6 +62,9 @@ importers:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.10.1
|
||||
version: 2.10.1
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
'@tauri-apps/plugin-process':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
@@ -1714,6 +1717,9 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.6.0':
|
||||
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
|
||||
|
||||
'@tauri-apps/plugin-process@2.3.1':
|
||||
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
|
||||
|
||||
@@ -5087,6 +5093,10 @@ snapshots:
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.10.0
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.10.0
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.6.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@tauri-apps/plugin-process@2.3.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
67
src-tauri/Cargo.lock
generated
67
src-tauri/Cargo.lock
generated
@@ -731,6 +731,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
@@ -1941,6 +1943,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-updater",
|
||||
@@ -3204,6 +3207,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -4119,6 +4146,46 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.8.0"
|
||||
|
||||
@@ -30,6 +30,7 @@ futures-util = "0.3"
|
||||
base64 = "0.22"
|
||||
regex = "1"
|
||||
dirs = "5"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:default",
|
||||
"updater:default",
|
||||
"process:default"
|
||||
]
|
||||
|
||||
@@ -134,6 +134,49 @@ fn clone_repo(url: String, token: String, local_path: String) -> Result<String,
|
||||
github::clone_repo(&url, &token, &local_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn create_vault_dir(path: String) -> Result<(), String> {
|
||||
std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_vault_dir_creates_directory() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let vault_path = tmp.path().join("my-new-vault");
|
||||
assert!(!vault_path.exists());
|
||||
|
||||
let result = create_vault_dir(vault_path.to_string_lossy().to_string());
|
||||
assert!(result.is_ok());
|
||||
assert!(vault_path.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_vault_dir_nested_path() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let vault_path = tmp.path().join("deep/nested/vault");
|
||||
assert!(!vault_path.exists());
|
||||
|
||||
let result = create_vault_dir(vault_path.to_string_lossy().to_string());
|
||||
assert!(result.is_ok());
|
||||
assert!(vault_path.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_vault_dir_existing_dir_ok() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let vault_path = tmp.path().join("existing");
|
||||
std::fs::create_dir(&vault_path).unwrap();
|
||||
|
||||
// Calling create_vault_dir on an existing dir should succeed
|
||||
let result = create_vault_dir(vault_path.to_string_lossy().to_string());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -146,6 +189,8 @@ pub fn run() {
|
||||
)?;
|
||||
}
|
||||
|
||||
app.handle().plugin(tauri_plugin_dialog::init())?;
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
app.handle()
|
||||
@@ -204,7 +249,8 @@ pub fn run() {
|
||||
save_settings,
|
||||
github_list_repos,
|
||||
github_create_repo,
|
||||
clone_repo
|
||||
clone_repo,
|
||||
create_vault_dir
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
47
src/App.tsx
47
src/App.tsx
@@ -17,7 +17,13 @@ import { useEditorSave } from './hooks/useEditorSave'
|
||||
import { useAppKeyboard } from './hooks/useAppKeyboard'
|
||||
import { useViewMode } from './hooks/useViewMode'
|
||||
import { useEntryActions } from './hooks/useEntryActions'
|
||||
import { isTauri } from './mock-tauri'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from './mock-tauri'
|
||||
import { pickFolder } from './utils/vault-dialog'
|
||||
|
||||
function tauriCall<T>(command: string, args: Record<string, unknown>): Promise<T> {
|
||||
return isTauri() ? invoke<T>(command, args) : mockInvoke<T>(command, args)
|
||||
}
|
||||
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'
|
||||
import { useUpdater } from './hooks/useUpdater'
|
||||
import { UpdateBanner } from './components/UpdateBanner'
|
||||
@@ -118,6 +124,43 @@ function App() {
|
||||
setToastMessage(`Vault "${label}" cloned and opened`)
|
||||
}, [handleSwitchVault])
|
||||
|
||||
const addAndSwitchVault = useCallback((path: string, label: string) => {
|
||||
setExtraVaults(prev => {
|
||||
if (prev.some(v => v.path === path)) return prev
|
||||
return [...prev, { label, path }]
|
||||
})
|
||||
handleSwitchVault(path)
|
||||
}, [handleSwitchVault])
|
||||
|
||||
const handleOpenLocalFolder = useCallback(async () => {
|
||||
try {
|
||||
const path = await pickFolder('Open vault folder')
|
||||
if (!path) return
|
||||
const label = path.split('/').pop() || 'Local Vault'
|
||||
addAndSwitchVault(path, label)
|
||||
setToastMessage(`Vault "${label}" opened`)
|
||||
} catch (err) {
|
||||
console.error('Failed to open local folder:', err)
|
||||
setToastMessage(`Failed to open folder: ${err}`)
|
||||
}
|
||||
}, [addAndSwitchVault])
|
||||
|
||||
const handleCreateNewVault = useCallback(async () => {
|
||||
try {
|
||||
const name = prompt('New vault name:')
|
||||
if (!name) return
|
||||
const parentDir = await pickFolder('Choose location for new vault')
|
||||
if (!parentDir) return
|
||||
const fullPath = `${parentDir}/${name}`
|
||||
await tauriCall('create_vault_dir', { path: fullPath })
|
||||
addAndSwitchVault(fullPath, name)
|
||||
setToastMessage(`Vault "${name}" created`)
|
||||
} catch (err) {
|
||||
console.error('Failed to create vault:', err)
|
||||
setToastMessage(`Failed to create vault: ${err}`)
|
||||
}
|
||||
}, [addAndSwitchVault])
|
||||
|
||||
useEffect(() => {
|
||||
if (!notes.activeTabPath) { setGitHistory([]); return }
|
||||
vault.loadGitHistory(notes.activeTabPath).then(setGitHistory)
|
||||
@@ -228,7 +271,7 @@ function App() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBar noteCount={vault.entries.length} vaultPath={vaultPath} vaults={allVaults} onSwitchVault={handleSwitchVault} onOpenSettings={() => setShowSettings(true)} onConnectGitHub={() => setShowGitHubVault(true)} hasGitHub={!!settings.github_token} />
|
||||
<StatusBar noteCount={vault.entries.length} vaultPath={vaultPath} vaults={allVaults} onSwitchVault={handleSwitchVault} onOpenSettings={() => setShowSettings(true)} onOpenLocalFolder={handleOpenLocalFolder} onCreateNewVault={handleCreateNewVault} onConnectGitHub={() => setShowGitHubVault(true)} hasGitHub={!!settings.github_token} />
|
||||
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
|
||||
<QuickOpenPalette open={showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={() => setShowQuickOpen(false)} />
|
||||
<CreateTypeDialog open={showCreateTypeDialog} onClose={() => setShowCreateTypeDialog(false)} onCreate={handleCreateType} />
|
||||
|
||||
@@ -73,4 +73,68 @@ describe('StatusBar', () => {
|
||||
fireEvent.click(vaultButton)
|
||||
expect(screen.queryByText('Work Vault')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows "Open local folder" option in vault menu', () => {
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onOpenLocalFolder={vi.fn()} />
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Switch vault'))
|
||||
expect(screen.getByText('Open local folder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onOpenLocalFolder when clicking "Open local folder"', () => {
|
||||
const onOpenLocalFolder = vi.fn()
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onOpenLocalFolder={onOpenLocalFolder} />
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Switch vault'))
|
||||
fireEvent.click(screen.getByText('Open local folder'))
|
||||
expect(onOpenLocalFolder).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows "Create new vault" option in vault menu', () => {
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onCreateNewVault={vi.fn()} />
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Switch vault'))
|
||||
expect(screen.getByText('Create new vault')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onCreateNewVault when clicking "Create new vault"', () => {
|
||||
const onCreateNewVault = vi.fn()
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onCreateNewVault={onCreateNewVault} />
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Switch vault'))
|
||||
fireEvent.click(screen.getByText('Create new vault'))
|
||||
expect(onCreateNewVault).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows all three add-vault options together', () => {
|
||||
render(
|
||||
<StatusBar
|
||||
noteCount={100}
|
||||
vaultPath="/Users/luca/Laputa"
|
||||
vaults={vaults}
|
||||
onSwitchVault={vi.fn()}
|
||||
onOpenLocalFolder={vi.fn()}
|
||||
onCreateNewVault={vi.fn()}
|
||||
onConnectGitHub={vi.fn()}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Switch vault'))
|
||||
expect(screen.getByText('Open local folder')).toBeInTheDocument()
|
||||
expect(screen.getByText('Create new vault')).toBeInTheDocument()
|
||||
expect(screen.getByText('Connect GitHub repo')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('closes menu after clicking "Open local folder"', () => {
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onOpenLocalFolder={vi.fn()} />
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Switch vault'))
|
||||
fireEvent.click(screen.getByText('Open local folder'))
|
||||
// Menu should close after clicking an action
|
||||
expect(screen.queryByText('Open local folder')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Package, GitBranch, RefreshCw, Sparkles, FileText, Bell, Settings, FolderOpen, Check, Github } from 'lucide-react'
|
||||
import { Package, GitBranch, RefreshCw, Sparkles, FileText, Bell, Settings, FolderOpen, Check, Github, FolderPlus } from 'lucide-react'
|
||||
|
||||
export interface VaultOption {
|
||||
label: string
|
||||
@@ -12,6 +12,8 @@ interface StatusBarProps {
|
||||
vaults: VaultOption[]
|
||||
onSwitchVault: (path: string) => void
|
||||
onOpenSettings?: () => void
|
||||
onOpenLocalFolder?: () => void
|
||||
onCreateNewVault?: () => void
|
||||
onConnectGitHub?: () => void
|
||||
hasGitHub?: boolean
|
||||
}
|
||||
@@ -34,7 +36,7 @@ function VaultMenuItem({ vault, isActive, onSelect }: { vault: VaultOption; isAc
|
||||
)
|
||||
}
|
||||
|
||||
function VaultMenu({ vaults, vaultPath, onSwitchVault, onConnectGitHub, hasGitHub }: { vaults: VaultOption[]; vaultPath: string; onSwitchVault: (path: string) => void; onConnectGitHub?: () => void; hasGitHub?: boolean }) {
|
||||
function VaultMenu({ vaults, vaultPath, onSwitchVault, onOpenLocalFolder, onCreateNewVault, onConnectGitHub, hasGitHub }: { vaults: VaultOption[]; vaultPath: string; onSwitchVault: (path: string) => void; onOpenLocalFolder?: () => void; onCreateNewVault?: () => void; onConnectGitHub?: () => void; hasGitHub?: boolean }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const activeVault = vaults.find((v) => v.path === vaultPath)
|
||||
@@ -57,25 +59,57 @@ function VaultMenu({ vaults, vaultPath, onSwitchVault, onConnectGitHub, hasGitHu
|
||||
{open && (
|
||||
<div style={{ position: 'absolute', bottom: '100%', left: 0, marginBottom: 4, background: 'var(--sidebar)', border: '1px solid var(--border)', borderRadius: 6, padding: 4, minWidth: 200, boxShadow: '0 4px 12px rgba(0,0,0,0.3)', zIndex: 1000 }}>
|
||||
{vaults.map((v) => <VaultMenuItem key={v.path} vault={v} isActive={v.path === vaultPath} onSelect={() => { onSwitchVault(v.path); setOpen(false) }} />)}
|
||||
<div style={{ height: 1, background: 'var(--border)', margin: '4px 0' }} />
|
||||
{onOpenLocalFolder && (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => { onOpenLocalFolder(); setOpen(false) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', borderRadius: 4,
|
||||
cursor: 'pointer', background: 'transparent',
|
||||
color: 'var(--muted-foreground)', fontSize: 12,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||
data-testid="vault-menu-open-local"
|
||||
>
|
||||
<FolderOpen size={12} />
|
||||
Open local folder
|
||||
</div>
|
||||
)}
|
||||
{onCreateNewVault && (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => { onCreateNewVault(); setOpen(false) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', borderRadius: 4,
|
||||
cursor: 'pointer', background: 'transparent',
|
||||
color: 'var(--muted-foreground)', fontSize: 12,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||
data-testid="vault-menu-create-new"
|
||||
>
|
||||
<FolderPlus size={12} />
|
||||
Create new vault
|
||||
</div>
|
||||
)}
|
||||
{onConnectGitHub && (
|
||||
<>
|
||||
<div style={{ height: 1, background: 'var(--border)', margin: '4px 0' }} />
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => { onConnectGitHub(); setOpen(false) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', borderRadius: 4,
|
||||
cursor: 'pointer', background: 'transparent',
|
||||
color: hasGitHub ? 'var(--muted-foreground)' : 'var(--accent-blue)', fontSize: 12,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||
data-testid="vault-menu-connect-github"
|
||||
>
|
||||
<Github size={12} />
|
||||
Connect GitHub repo
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => { onConnectGitHub(); setOpen(false) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', borderRadius: 4,
|
||||
cursor: 'pointer', background: 'transparent',
|
||||
color: hasGitHub ? 'var(--muted-foreground)' : 'var(--accent-blue)', fontSize: 12,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||
data-testid="vault-menu-connect-github"
|
||||
>
|
||||
<Github size={12} />
|
||||
Connect GitHub repo
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -87,11 +121,11 @@ const ICON_STYLE = { display: 'flex', alignItems: 'center', gap: 4 } as const
|
||||
const DISABLED_STYLE = { display: 'flex', alignItems: 'center', opacity: 0.4, cursor: 'not-allowed' } as const
|
||||
const SEP_STYLE = { color: 'var(--border)' } as const
|
||||
|
||||
export function StatusBar({ noteCount, vaultPath, vaults, onSwitchVault, onOpenSettings, onConnectGitHub, hasGitHub }: StatusBarProps) {
|
||||
export function StatusBar({ noteCount, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onCreateNewVault, onConnectGitHub, hasGitHub }: StatusBarProps) {
|
||||
return (
|
||||
<footer style={{ height: 30, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: 'var(--sidebar)', borderTop: '1px solid var(--border)', padding: '0 8px', fontSize: 11, color: 'var(--muted-foreground)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<VaultMenu vaults={vaults} vaultPath={vaultPath} onSwitchVault={onSwitchVault} onConnectGitHub={onConnectGitHub} hasGitHub={hasGitHub} />
|
||||
<VaultMenu vaults={vaults} vaultPath={vaultPath} onSwitchVault={onSwitchVault} onOpenLocalFolder={onOpenLocalFolder} onCreateNewVault={onCreateNewVault} onConnectGitHub={onConnectGitHub} hasGitHub={hasGitHub} />
|
||||
<span style={SEP_STYLE}>|</span>
|
||||
<span style={ICON_STYLE}><Package size={13} />v0.4.2</span>
|
||||
<span style={SEP_STYLE}>|</span>
|
||||
|
||||
@@ -1760,6 +1760,7 @@ const mockHandlers: Record<string, (args: any) => any> = {
|
||||
clone_repo: (args: { url: string; local_path: string }) => `Cloned to ${args.local_path}`,
|
||||
purge_trash: () => [],
|
||||
migrate_is_a_to_type: () => 0,
|
||||
create_vault_dir: () => null,
|
||||
}
|
||||
|
||||
export function isTauri(): boolean {
|
||||
|
||||
40
src/utils/vault-dialog.test.ts
Normal file
40
src/utils/vault-dialog.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Mock isTauri — default to browser mode
|
||||
vi.mock('../mock-tauri', () => ({
|
||||
isTauri: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
import { pickFolder } from './vault-dialog'
|
||||
import { isTauri } from '../mock-tauri'
|
||||
|
||||
describe('pickFolder', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('returns user input from prompt in browser mode', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(false)
|
||||
vi.spyOn(window, 'prompt').mockReturnValue('/Users/test/my-vault')
|
||||
|
||||
const result = await pickFolder('Select vault')
|
||||
expect(result).toBe('/Users/test/my-vault')
|
||||
expect(window.prompt).toHaveBeenCalledWith('Select vault')
|
||||
})
|
||||
|
||||
it('returns null when user cancels prompt in browser mode', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(false)
|
||||
vi.spyOn(window, 'prompt').mockReturnValue(null)
|
||||
|
||||
const result = await pickFolder('Select vault')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('uses default title when none provided in browser mode', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(false)
|
||||
vi.spyOn(window, 'prompt').mockReturnValue('/some/path')
|
||||
|
||||
await pickFolder()
|
||||
expect(window.prompt).toHaveBeenCalledWith('Enter folder path:')
|
||||
})
|
||||
})
|
||||
25
src/utils/vault-dialog.ts
Normal file
25
src/utils/vault-dialog.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Vault dialog utilities.
|
||||
* In Tauri mode, uses the native dialog plugin for folder picking.
|
||||
* In browser mode, falls back to window.prompt() for testing.
|
||||
*/
|
||||
|
||||
import { isTauri } from '../mock-tauri'
|
||||
|
||||
/**
|
||||
* Opens a native folder picker dialog (Tauri) or falls back to prompt (browser).
|
||||
* Returns the selected folder path, or null if the user cancelled.
|
||||
*/
|
||||
export async function pickFolder(title?: string): Promise<string | null> {
|
||||
if (isTauri()) {
|
||||
const { open } = await import('@tauri-apps/plugin-dialog')
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: title ?? 'Select folder',
|
||||
})
|
||||
return selected as string | null
|
||||
}
|
||||
// Browser fallback: prompt for path
|
||||
return prompt(title ?? 'Enter folder path:')
|
||||
}
|
||||
Reference in New Issue
Block a user