feat: add empty vault creation flow

This commit is contained in:
lucaronin
2026-04-19 01:06:40 +02:00
parent aed4d05f1d
commit 67a9930ea1
18 changed files with 450 additions and 79 deletions

View File

@@ -154,6 +154,24 @@ describe('StatusBar', () => {
expect(onOpenLocalFolder).toHaveBeenCalledOnce()
})
it('shows "Create empty vault" option in vault menu', () => {
render(
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onCreateEmptyVault={vi.fn()} />
)
fireEvent.click(screen.getByRole('button', { name: 'Switch vault' }))
expect(screen.getByText('Create empty vault')).toBeInTheDocument()
})
it('calls onCreateEmptyVault when clicking "Create empty vault"', () => {
const onCreateEmptyVault = vi.fn()
render(
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onCreateEmptyVault={onCreateEmptyVault} />
)
fireEvent.click(screen.getByRole('button', { name: 'Switch vault' }))
fireEvent.click(screen.getByText('Create empty vault'))
expect(onCreateEmptyVault).toHaveBeenCalledOnce()
})
it('shows add-vault options in vault menu', () => {
render(
<StatusBar
@@ -161,11 +179,13 @@ describe('StatusBar', () => {
vaultPath="/Users/luca/Laputa"
vaults={vaults}
onSwitchVault={vi.fn()}
onCreateEmptyVault={vi.fn()}
onOpenLocalFolder={vi.fn()}
onCloneVault={vi.fn()}
/>
)
fireEvent.click(screen.getByRole('button', { name: 'Switch vault' }))
expect(screen.getByText('Create empty vault')).toBeInTheDocument()
expect(screen.getByText('Open local folder')).toBeInTheDocument()
expect(screen.getByText('Clone Git repo')).toBeInTheDocument()
})

View File

@@ -21,6 +21,7 @@ interface StatusBarProps {
onSwitchVault: (path: string) => void
onOpenSettings?: () => void
onOpenLocalFolder?: () => void
onCreateEmptyVault?: () => void
onCloneVault?: () => void
onCloneGettingStarted?: () => void
onClickPending?: () => void
@@ -60,6 +61,7 @@ export function StatusBar({
onSwitchVault,
onOpenSettings,
onOpenLocalFolder,
onCreateEmptyVault,
onCloneVault,
onCloneGettingStarted,
onClickPending,
@@ -121,6 +123,7 @@ export function StatusBar({
vaults={vaults}
onSwitchVault={onSwitchVault}
onOpenLocalFolder={onOpenLocalFolder}
onCreateEmptyVault={onCreateEmptyVault}
onCloneVault={onCloneVault}
onCloneGettingStarted={onCloneGettingStarted}
onClickPending={onClickPending}

View File

@@ -13,7 +13,7 @@ const defaultProps = {
defaultVaultPath: '~/Documents/Laputa',
onCreateVault: vi.fn(),
onRetryCreateVault: vi.fn(),
onCreateNewVault: vi.fn(),
onCreateEmptyVault: vi.fn(),
onOpenFolder: vi.fn(),
isOffline: false,
creatingAction: null as 'template' | 'empty' | null,
@@ -35,7 +35,7 @@ describe('WelcomeScreen', () => {
it('shows all three option buttons', () => {
render(<WelcomeScreen {...defaultProps} />)
expect(screen.getByTestId('welcome-create-new')).toHaveTextContent('Create a new vault')
expect(screen.getByTestId('welcome-create-new')).toHaveTextContent('Create empty vault')
expect(screen.getByTestId('welcome-open-folder')).toHaveTextContent('Open existing vault')
expect(screen.getByTestId('welcome-create-vault')).toHaveTextContent('Get started with a template')
})
@@ -56,11 +56,11 @@ describe('WelcomeScreen', () => {
expect(screen.getByText(/Requires internet — clone later/)).toBeInTheDocument()
})
it('calls onCreateNewVault when create new button is clicked', () => {
const onCreateNewVault = vi.fn()
render(<WelcomeScreen {...defaultProps} onCreateNewVault={onCreateNewVault} />)
it('calls onCreateEmptyVault when create empty button is clicked', () => {
const onCreateEmptyVault = vi.fn()
render(<WelcomeScreen {...defaultProps} onCreateEmptyVault={onCreateEmptyVault} />)
fireEvent.click(screen.getByTestId('welcome-create-new'))
expect(onCreateNewVault).toHaveBeenCalledOnce()
expect(onCreateEmptyVault).toHaveBeenCalledOnce()
})
it('calls onCreateVault when template button is clicked', () => {

View File

@@ -9,7 +9,7 @@ interface WelcomeScreenProps {
defaultVaultPath: string
onCreateVault: () => void
onRetryCreateVault: () => void
onCreateNewVault: () => void
onCreateEmptyVault: () => void
onOpenFolder: () => void
isOffline: boolean
creatingAction: 'template' | 'empty' | null
@@ -230,7 +230,7 @@ export function WelcomeScreen({
defaultVaultPath,
onCreateVault,
onRetryCreateVault,
onCreateNewVault,
onCreateEmptyVault,
onOpenFolder,
isOffline,
creatingAction,
@@ -269,11 +269,11 @@ export function WelcomeScreen({
<OptionButton
icon={<Plus size={18} style={{ color: 'var(--accent-blue)' }} />}
iconBg="var(--accent-blue-light, #EBF4FF)"
label="Create a new vault"
description="Start fresh in a folder you choose"
label="Create empty vault"
description="Start fresh in an empty folder with Tolaria defaults"
loadingLabel="Creating vault…"
loadingDescription="Preparing an empty vault in the selected folder"
onClick={onCreateNewVault}
loadingDescription="Preparing Tolaria defaults in the selected folder"
onClick={onCreateEmptyVault}
disabled={busy}
loading={creatingAction === 'empty'}
testId="welcome-create-new"

View File

@@ -35,6 +35,7 @@ interface StatusBarPrimarySectionProps {
vaults: VaultOption[]
onSwitchVault: (path: string) => void
onOpenLocalFolder?: () => void
onCreateEmptyVault?: () => void
onCloneVault?: () => void
onCloneGettingStarted?: () => void
onClickPending?: () => void
@@ -77,6 +78,7 @@ export function StatusBarPrimarySection({
vaults,
onSwitchVault,
onOpenLocalFolder,
onCreateEmptyVault,
onCloneVault,
onCloneGettingStarted,
onClickPending,
@@ -111,6 +113,7 @@ export function StatusBarPrimarySection({
vaultPath={vaultPath}
onSwitchVault={onSwitchVault}
onOpenLocalFolder={onOpenLocalFolder}
onCreateEmptyVault={onCreateEmptyVault}
onCloneVault={onCloneVault}
onCloneGettingStarted={onCloneGettingStarted}
onRemoveVault={onRemoveVault}

View File

@@ -1,6 +1,6 @@
import { useMemo, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import { AlertTriangle, Check, FolderOpen, GitBranch, Rocket, X } from 'lucide-react'
import { AlertTriangle, Check, FolderOpen, GitBranch, Plus, Rocket, X } from 'lucide-react'
import { ActionTooltip } from '@/components/ui/action-tooltip'
import { Button } from '@/components/ui/button'
import type { VaultOption } from './types'
@@ -11,6 +11,7 @@ interface VaultMenuProps {
vaultPath: string
onSwitchVault: (path: string) => void
onOpenLocalFolder?: () => void
onCreateEmptyVault?: () => void
onCloneVault?: () => void
onCloneGettingStarted?: () => void
onRemoveVault?: (path: string) => void
@@ -42,12 +43,24 @@ interface VaultAction {
}
function buildVaultActions({
onCreateEmptyVault,
onCloneGettingStarted,
onCloneVault,
onOpenLocalFolder,
}: Pick<VaultMenuProps, 'onCloneGettingStarted' | 'onCloneVault' | 'onOpenLocalFolder'>): VaultAction[] {
}: Pick<VaultMenuProps, 'onCreateEmptyVault' | 'onCloneGettingStarted' | 'onCloneVault' | 'onOpenLocalFolder'>): VaultAction[] {
const items: VaultAction[] = []
if (onCreateEmptyVault) {
items.push({
key: 'create-empty',
icon: <Plus size={12} />,
label: 'Create empty vault',
testId: 'vault-menu-create-empty',
accent: true,
onClick: onCreateEmptyVault,
})
}
if (onOpenLocalFolder) {
items.push({
key: 'open-local',
@@ -158,6 +171,7 @@ export function VaultMenu({
vaultPath,
onSwitchVault,
onOpenLocalFolder,
onCreateEmptyVault,
onCloneVault,
onCloneGettingStarted,
onRemoveVault,
@@ -171,11 +185,12 @@ export function VaultMenu({
const actions = useMemo<VaultAction[]>(() => {
return buildVaultActions({
onCreateEmptyVault,
onCloneGettingStarted,
onCloneVault,
onOpenLocalFolder,
})
}, [onCloneGettingStarted, onCloneVault, onOpenLocalFolder])
}, [onCreateEmptyVault, onCloneGettingStarted, onCloneVault, onOpenLocalFolder])
return (
<div ref={menuRef} style={{ position: 'relative' }}>