feat: add local vault options to vault picker
Add "Open local folder" and "Create new vault" options to the vault picker dropdown. Uses tauri-plugin-dialog for native folder picker in Tauri mode, falls back to prompt() in browser mode. - Rust: add tauri-plugin-dialog, create_vault_dir command - Frontend: pickFolder utility, StatusBar UI, App.tsx handlers - Mock: create_vault_dir mock handler for browser testing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,11 @@ 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_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -146,6 +151,8 @@ pub fn run() {
|
||||
)?;
|
||||
}
|
||||
|
||||
app.handle().plugin(tauri_plugin_dialog::init())?;
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
app.handle()
|
||||
@@ -204,7 +211,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} />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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