fix: address folder tree QA feedback — recursive filtering, folder creation, path display, remove flatten banner
1. Folder selection now recursively includes notes from subfolders 2. + button creates a new folder with inline rename (Tauri command + UI) 3. Note list items show full path when note is in a subdirectory 4. Remove flat vault migration banner and all related code (Rust commands, hook, component, smoke test) — subfolders are now first-class Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
interface FlatVaultMigrationBannerProps {
|
||||
strayFileCount: number
|
||||
isMigrating: boolean
|
||||
onMigrate: () => void
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner shown when the vault has files in non-protected subfolders.
|
||||
* Offers to flatten them to the vault root.
|
||||
*/
|
||||
export function FlatVaultMigrationBanner({ strayFileCount, isMigrating, onMigrate, onDismiss }: FlatVaultMigrationBannerProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-2 bg-amber-50 dark:bg-amber-950 border-b border-amber-200 dark:border-amber-800 text-sm" data-testid="migration-banner">
|
||||
<span className="flex-1 text-amber-800 dark:text-amber-200">
|
||||
{strayFileCount} note{strayFileCount !== 1 ? 's' : ''} found in subfolders.
|
||||
Flatten to vault root for consistent scanning.
|
||||
</span>
|
||||
<button
|
||||
className="px-3 py-1 text-xs font-medium rounded bg-amber-600 text-white hover:bg-amber-700 disabled:opacity-50"
|
||||
onClick={onMigrate}
|
||||
disabled={isMigrating}
|
||||
data-testid="migration-flatten-btn"
|
||||
>
|
||||
{isMigrating ? 'Migrating...' : 'Flatten Now'}
|
||||
</button>
|
||||
<button
|
||||
className="px-2 py-1 text-xs text-amber-600 dark:text-amber-400 hover:underline"
|
||||
onClick={onDismiss}
|
||||
disabled={isMigrating}
|
||||
data-testid="migration-dismiss-btn"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, memo } from 'react'
|
||||
import { useState, useCallback, useRef, useEffect, memo } from 'react'
|
||||
import { Folder, FolderOpen, CaretDown, CaretRight, Plus } from '@phosphor-icons/react'
|
||||
import type { FolderNode, SidebarSelection } from '../types'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -7,6 +7,7 @@ interface FolderTreeProps {
|
||||
folders: FolderNode[]
|
||||
selection: SidebarSelection
|
||||
onSelect: (selection: SidebarSelection) => void
|
||||
onCreateFolder?: (name: string) => void
|
||||
}
|
||||
|
||||
function FolderItem({
|
||||
@@ -71,15 +72,31 @@ function FolderItem({
|
||||
)
|
||||
}
|
||||
|
||||
export const FolderTree = memo(function FolderTree({ folders, selection, onSelect }: FolderTreeProps) {
|
||||
export const FolderTree = memo(function FolderTree({ folders, selection, onSelect, onCreateFolder }: FolderTreeProps) {
|
||||
const [sectionCollapsed, setSectionCollapsed] = useState(false)
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [newFolderName, setNewFolderName] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const toggleFolder = useCallback((path: string) => {
|
||||
setExpanded((prev) => ({ ...prev, [path]: !prev[path] }))
|
||||
}, [])
|
||||
|
||||
if (folders.length === 0) return null
|
||||
useEffect(() => {
|
||||
if (isCreating) inputRef.current?.focus()
|
||||
}, [isCreating])
|
||||
|
||||
const handleCreateFolder = () => {
|
||||
const name = newFolderName.trim()
|
||||
if (name && onCreateFolder) {
|
||||
onCreateFolder(name)
|
||||
}
|
||||
setIsCreating(false)
|
||||
setNewFolderName('')
|
||||
}
|
||||
|
||||
if (folders.length === 0 && !isCreating) return null
|
||||
|
||||
return (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
@@ -93,7 +110,14 @@ export const FolderTree = memo(function FolderTree({ folders, selection, onSelec
|
||||
{sectionCollapsed ? <CaretRight size={12} /> : <CaretDown size={12} />}
|
||||
<span className="text-[10px] font-semibold" style={{ letterSpacing: 0.5 }}>FOLDERS</span>
|
||||
</div>
|
||||
<Plus size={12} className="text-muted-foreground" />
|
||||
{onCreateFolder && (
|
||||
<Plus
|
||||
size={12}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={(e) => { e.stopPropagation(); setIsCreating(true); setSectionCollapsed(false) }}
|
||||
data-testid="create-folder-btn"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Tree */}
|
||||
@@ -110,6 +134,24 @@ export const FolderTree = memo(function FolderTree({ folders, selection, onSelec
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
{isCreating && (
|
||||
<div className="flex items-center gap-2" style={{ padding: '4px 8px' }}>
|
||||
<Folder size={18} className="shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="flex-1 border border-border rounded bg-background px-1.5 py-0.5 text-[13px] text-foreground outline-none focus:border-primary"
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleCreateFolder()
|
||||
if (e.key === 'Escape') { setIsCreating(false); setNewFolderName('') }
|
||||
}}
|
||||
onBlur={handleCreateFolder}
|
||||
placeholder="Folder name"
|
||||
data-testid="new-folder-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -129,6 +129,9 @@ export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlig
|
||||
{entry.title}
|
||||
<StateBadge archived={entry.archived} trashed={entry.trashed} />
|
||||
</div>
|
||||
{entry.path.includes('/') && (
|
||||
<div className="truncate text-[10px] text-muted-foreground" data-testid="note-path">{entry.path}</div>
|
||||
)}
|
||||
</div>
|
||||
{entry.snippet && (
|
||||
<div className="mt-0.5 text-[12px] leading-[1.5] text-muted-foreground" data-testid="note-snippet" style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
|
||||
@@ -35,6 +35,7 @@ interface SidebarProps {
|
||||
onRenameSection?: (typeName: string, label: string) => void
|
||||
onToggleTypeVisibility?: (typeName: string) => void
|
||||
folders?: FolderNode[]
|
||||
onCreateFolder?: (name: string) => void
|
||||
inboxCount?: number
|
||||
onCollapse?: () => void
|
||||
}
|
||||
@@ -207,7 +208,7 @@ export const Sidebar = memo(function Sidebar({
|
||||
entries, selection, onSelect, onSelectNote, onCreateType, onCreateNewType,
|
||||
onCustomizeType, onUpdateTypeTemplate, onReorderSections, onRenameSection,
|
||||
onToggleTypeVisibility,
|
||||
folders = [], inboxCount = 0, onCollapse,
|
||||
folders = [], onCreateFolder, inboxCount = 0, onCollapse,
|
||||
}: SidebarProps) {
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
||||
const [customizeTarget, setCustomizeTarget] = useState<string | null>(null)
|
||||
@@ -317,7 +318,7 @@ export const Sidebar = memo(function Sidebar({
|
||||
</DndContext>
|
||||
|
||||
{/* Folder tree */}
|
||||
<FolderTree folders={folders} selection={selection} onSelect={onSelect} />
|
||||
<FolderTree folders={folders} selection={selection} onSelect={onSelect} onCreateFolder={onCreateFolder} />
|
||||
</nav>
|
||||
|
||||
<ContextMenuOverlay pos={contextMenuPos} type={contextMenuType} innerRef={contextMenuRef} onOpenCustomize={(type) => { closeContextMenu(); setCustomizeTarget(type) }} onStartRename={handleStartRename} />
|
||||
|
||||
Reference in New Issue
Block a user