mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
fix: context-aware folder/note creation in knowledge panel (#538)
* fix: context-aware folder/note creation with folder highlight and inline rename * fix: clear folder highlight when a note is opened
This commit is contained in:
parent
eb6a7ac466
commit
4b7911c8ea
3 changed files with 133 additions and 22 deletions
|
|
@ -477,7 +477,8 @@ function flattenMeetingsTree(nodes: TreeNode[]): TreeNode[] {
|
|||
}
|
||||
collectFiles(sourceNode, [])
|
||||
|
||||
if (dateGroups.size === 0) return []
|
||||
// Pass through user-created folders that have no meeting-style date files
|
||||
if (dateGroups.size === 0) return [sourceNode]
|
||||
|
||||
// Build date folder nodes, sorted reverse chronologically
|
||||
const dateFolderNodes: TreeNode[] = [...dateGroups.entries()]
|
||||
|
|
@ -3770,7 +3771,7 @@ function App() {
|
|||
}, [])
|
||||
|
||||
const knowledgeActions = React.useMemo(() => ({
|
||||
createNote: async (parentPath: string = 'knowledge/Notes') => {
|
||||
createNote: async (parentPath: string = 'knowledge') => {
|
||||
try {
|
||||
let index = 0
|
||||
let name = untitledBaseName
|
||||
|
|
@ -3787,18 +3788,22 @@ function App() {
|
|||
data: `# ${name}\n\n`,
|
||||
opts: { encoding: 'utf8' }
|
||||
})
|
||||
setExpandedPaths(prev => new Set([...prev, parentPath]))
|
||||
navigateToFile(fullPath)
|
||||
} catch (err) {
|
||||
console.error('Failed to create note:', err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
createFolder: async (parentPath: string = 'knowledge/Notes') => {
|
||||
createFolder: async (parentPath: string = 'knowledge'): Promise<string> => {
|
||||
const newPath = `${parentPath}/new-folder-${Date.now()}`
|
||||
try {
|
||||
await window.ipc.invoke('workspace:mkdir', {
|
||||
path: `${parentPath}/new-folder-${Date.now()}`,
|
||||
path: newPath,
|
||||
recursive: true
|
||||
})
|
||||
setExpandedPaths(prev => new Set([...prev, parentPath]))
|
||||
return newPath
|
||||
} catch (err) {
|
||||
console.error('Failed to create folder:', err)
|
||||
throw err
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ interface TreeNode {
|
|||
|
||||
type KnowledgeActions = {
|
||||
createNote: (parentPath?: string) => void
|
||||
createFolder: (parentPath?: string) => void
|
||||
createFolder: (parentPath?: string) => Promise<string>
|
||||
openGraph: () => void
|
||||
openBases: () => void
|
||||
expandAll: () => void
|
||||
|
|
@ -1111,6 +1111,8 @@ function KnowledgeSection({
|
|||
}) {
|
||||
const isExpanded = expandedPaths.size > 0
|
||||
const treeContainerRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
|
||||
const [renameTarget, setRenameTarget] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPath) return
|
||||
|
|
@ -1141,9 +1143,40 @@ function KnowledgeSection({
|
|||
}
|
||||
}, [selectedPath, expandedPaths, tree])
|
||||
|
||||
// Folder clicks highlight the folder; file clicks clear folder highlight
|
||||
const handleSelect = React.useCallback((path: string, kind: "file" | "dir") => {
|
||||
if (kind === 'dir') {
|
||||
setSelectedFolderPath(path)
|
||||
} else {
|
||||
setSelectedFolderPath(null)
|
||||
}
|
||||
onSelectFile(path, kind)
|
||||
}, [onSelectFile])
|
||||
|
||||
// Resolve the parent path for new items: explicit folder > open file's parent > root
|
||||
const deriveParent = React.useCallback((): string => {
|
||||
if (selectedFolderPath) return selectedFolderPath
|
||||
if (selectedPath) {
|
||||
const parts = selectedPath.split('/')
|
||||
if (parts.length > 1) return parts.slice(0, -1).join('/')
|
||||
}
|
||||
return 'knowledge'
|
||||
}, [selectedFolderPath, selectedPath])
|
||||
|
||||
// Wrap actions to inject context-aware parent and capture rename target
|
||||
const wrappedActions = React.useMemo<KnowledgeActions>(() => ({
|
||||
...actions,
|
||||
createNote: (parentPath?: string) => actions.createNote(parentPath ?? deriveParent()),
|
||||
createFolder: async (parentPath?: string): Promise<string> => {
|
||||
const newPath = await actions.createFolder(parentPath ?? deriveParent())
|
||||
setRenameTarget(newPath)
|
||||
return newPath
|
||||
},
|
||||
}), [actions, deriveParent])
|
||||
|
||||
const quickActions = [
|
||||
{ icon: FilePlus, label: "New Note", action: () => actions.createNote() },
|
||||
{ icon: FolderPlus, label: "New Folder", action: () => actions.createFolder() },
|
||||
{ icon: FilePlus, label: "New Note", action: () => wrappedActions.createNote() },
|
||||
{ icon: FolderPlus, label: "New Folder", action: () => void wrappedActions.createFolder() },
|
||||
{ icon: Network, label: "Graph View", action: () => actions.openGraph() },
|
||||
{ icon: Table2, label: "Bases", action: () => actions.openBases() },
|
||||
]
|
||||
|
|
@ -1194,9 +1227,12 @@ function KnowledgeSection({
|
|||
item={item}
|
||||
selectedPath={selectedPath}
|
||||
expandedPaths={expandedPaths}
|
||||
onSelect={onSelectFile}
|
||||
onSelect={handleSelect}
|
||||
onToggleFolder={onToggleFolder}
|
||||
actions={actions}
|
||||
actions={wrappedActions}
|
||||
selectedFolderPath={selectedFolderPath}
|
||||
renameTarget={renameTarget}
|
||||
onRenameTargetConsumed={() => setRenameTarget(null)}
|
||||
/>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
|
|
@ -1205,11 +1241,11 @@ function KnowledgeSection({
|
|||
</SidebarGroup>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-48">
|
||||
<ContextMenuItem onClick={() => actions.createNote()}>
|
||||
<ContextMenuItem onClick={() => wrappedActions.createNote()}>
|
||||
<FilePlus className="mr-2 size-4" />
|
||||
New Note
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => actions.createFolder()}>
|
||||
<ContextMenuItem onClick={() => void wrappedActions.createFolder()}>
|
||||
<FolderPlus className="mr-2 size-4" />
|
||||
New Folder
|
||||
</ContextMenuItem>
|
||||
|
|
@ -1234,6 +1270,9 @@ function Tree({
|
|||
onSelect,
|
||||
onToggleFolder,
|
||||
actions,
|
||||
selectedFolderPath,
|
||||
renameTarget,
|
||||
onRenameTargetConsumed,
|
||||
}: {
|
||||
item: TreeNode
|
||||
selectedPath: string | null
|
||||
|
|
@ -1241,10 +1280,14 @@ function Tree({
|
|||
onSelect: (path: string, kind: "file" | "dir") => void
|
||||
onToggleFolder?: (path: string) => void
|
||||
actions: KnowledgeActions
|
||||
selectedFolderPath?: string | null
|
||||
renameTarget?: string | null
|
||||
onRenameTargetConsumed?: () => void
|
||||
}) {
|
||||
const isDir = item.kind === 'dir'
|
||||
const isExpanded = expandedPaths.has(item.path)
|
||||
const isSelected = selectedPath === item.path
|
||||
const isFolderSelected = isDir && selectedFolderPath === item.path
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const isSubmittingRef = React.useRef(false)
|
||||
const displayName = (isDir && FOLDER_DISPLAY_NAMES[item.name]) || item.name
|
||||
|
|
@ -1255,6 +1298,17 @@ function Tree({
|
|||
: item.name
|
||||
const [newName, setNewName] = useState(baseName)
|
||||
|
||||
// Auto-enter rename mode when this node is the rename target
|
||||
React.useEffect(() => {
|
||||
if (renameTarget === item.path) {
|
||||
setNewName(baseName)
|
||||
isSubmittingRef.current = false
|
||||
setIsRenaming(true)
|
||||
onRenameTargetConsumed?.()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [renameTarget, item.path])
|
||||
|
||||
// Sync newName when baseName changes (e.g., after external rename)
|
||||
React.useEffect(() => {
|
||||
setNewName(baseName)
|
||||
|
|
@ -1385,7 +1439,7 @@ function Tree({
|
|||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<SidebarMenuItem className="group/file-item">
|
||||
<SidebarMenuButton onClick={() => onSelect(item.path, item.kind)}>
|
||||
<SidebarMenuButton isActive={isFolderSelected} onClick={() => onSelect(item.path, item.kind)}>
|
||||
<Folder className="size-4 shrink-0" />
|
||||
<div className="flex w-full items-center gap-1 min-w-0">
|
||||
<span className="min-w-0 flex-1 truncate">{displayName}</span>
|
||||
|
|
@ -1420,6 +1474,9 @@ function Tree({
|
|||
onSelect={onSelect}
|
||||
onToggleFolder={onToggleFolder}
|
||||
actions={actions}
|
||||
selectedFolderPath={selectedFolderPath}
|
||||
renameTarget={renameTarget}
|
||||
onRenameTargetConsumed={onRenameTargetConsumed}
|
||||
/>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
|
|
@ -1471,7 +1528,7 @@ function Tree({
|
|||
className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90"
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton>
|
||||
<SidebarMenuButton isActive={isFolderSelected}>
|
||||
<ChevronRight className="transition-transform size-4" />
|
||||
<div className="flex w-full items-center gap-1 min-w-0">
|
||||
<span className="min-w-0 flex-1 truncate">{displayName}</span>
|
||||
|
|
@ -1490,6 +1547,9 @@ function Tree({
|
|||
onSelect={onSelect}
|
||||
onToggleFolder={onToggleFolder}
|
||||
actions={actions}
|
||||
selectedFolderPath={selectedFolderPath}
|
||||
renameTarget={renameTarget}
|
||||
onRenameTargetConsumed={onRenameTargetConsumed}
|
||||
/>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue