mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-03 19:25:19 +02:00
fix navigation and other minor issue in workspace view (#587)
This commit is contained in:
parent
5677916790
commit
78d51ccbf6
2 changed files with 94 additions and 45 deletions
|
|
@ -5506,7 +5506,11 @@ function App() {
|
||||||
remove: knowledgeActions.remove,
|
remove: knowledgeActions.remove,
|
||||||
copyPath: knowledgeActions.copyPath,
|
copyPath: knowledgeActions.copyPath,
|
||||||
revealInFileManager: knowledgeActions.revealInFileManager,
|
revealInFileManager: knowledgeActions.revealInFileManager,
|
||||||
|
createNote: knowledgeActions.createNote,
|
||||||
|
createFolder: knowledgeActions.createFolder,
|
||||||
|
onOpenInNewTab: knowledgeActions.onOpenInNewTab,
|
||||||
}}
|
}}
|
||||||
|
onNavigate={(path) => { void navigateToView({ type: 'workspace', path: path === WORKSPACE_ROOT ? undefined : path }) }}
|
||||||
onOpenNote={(path) => navigateToFile(path)}
|
onOpenNote={(path) => navigateToFile(path)}
|
||||||
onCreateWorkspace={async (name) => { await knowledgeActions.createWorkspace(name) }}
|
onCreateWorkspace={async (name) => { await knowledgeActions.createWorkspace(name) }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Copy,
|
Copy,
|
||||||
|
ExternalLink,
|
||||||
File as FileIcon,
|
File as FileIcon,
|
||||||
FilePlus,
|
FilePlus,
|
||||||
Folder as FolderIcon,
|
Folder as FolderIcon,
|
||||||
|
|
@ -53,12 +54,18 @@ type WorkspaceActions = {
|
||||||
remove: (path: string) => Promise<void>
|
remove: (path: string) => Promise<void>
|
||||||
copyPath: (path: string) => void
|
copyPath: (path: string) => void
|
||||||
revealInFileManager: (path: string, isDir: boolean) => void
|
revealInFileManager: (path: string, isDir: boolean) => void
|
||||||
|
createNote: (parentPath?: string) => void
|
||||||
|
createFolder: (parentPath?: string) => Promise<string>
|
||||||
|
onOpenInNewTab?: (path: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceViewProps = {
|
type WorkspaceViewProps = {
|
||||||
tree: TreeNode[]
|
tree: TreeNode[]
|
||||||
initialPath?: string | null
|
initialPath?: string | null
|
||||||
actions: WorkspaceActions
|
actions: WorkspaceActions
|
||||||
|
// Folder currently being browsed. Controlled by the app so drill-down
|
||||||
|
// participates in the global back/forward history.
|
||||||
|
onNavigate: (path: string) => void
|
||||||
onOpenNote: (path: string) => void
|
onOpenNote: (path: string) => void
|
||||||
onCreateWorkspace: (name: string) => Promise<void>
|
onCreateWorkspace: (name: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +78,12 @@ function getFileManagerName(): string {
|
||||||
return 'File Manager'
|
return 'File Manager'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileExtensionLabel(name: string): string {
|
||||||
|
const dot = name.lastIndexOf('.')
|
||||||
|
if (dot <= 0 || dot === name.length - 1) return 'File'
|
||||||
|
return `${name.slice(dot + 1).toUpperCase()} file`
|
||||||
|
}
|
||||||
|
|
||||||
function findNode(nodes: TreeNode[] | undefined, path: string): TreeNode | null {
|
function findNode(nodes: TreeNode[] | undefined, path: string): TreeNode | null {
|
||||||
if (!nodes) return null
|
if (!nodes) return null
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
|
@ -113,8 +126,8 @@ function readFileAsBase64(file: File): Promise<string> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreateWorkspace }: WorkspaceViewProps) {
|
export function WorkspaceView({ tree, initialPath, actions, onNavigate, onOpenNote, onCreateWorkspace }: WorkspaceViewProps) {
|
||||||
const [currentPath, setCurrentPath] = useState<string>(initialPath || WORKSPACE_ROOT)
|
const currentPath = initialPath || WORKSPACE_ROOT
|
||||||
const [addOpen, setAddOpen] = useState(false)
|
const [addOpen, setAddOpen] = useState(false)
|
||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
|
|
@ -127,10 +140,6 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
|
||||||
const filesInputRef = useRef<HTMLInputElement | null>(null)
|
const filesInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const folderInputRef = useRef<HTMLInputElement | null>(null)
|
const folderInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialPath) setCurrentPath(initialPath)
|
|
||||||
}, [initialPath])
|
|
||||||
|
|
||||||
const isRoot = currentPath === WORKSPACE_ROOT
|
const isRoot = currentPath === WORKSPACE_ROOT
|
||||||
const fileManagerName = getFileManagerName()
|
const fileManagerName = getFileManagerName()
|
||||||
|
|
||||||
|
|
@ -160,12 +169,12 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
|
||||||
(item: TreeNode) => {
|
(item: TreeNode) => {
|
||||||
if (renameTarget) return
|
if (renameTarget) return
|
||||||
if (item.kind === 'dir') {
|
if (item.kind === 'dir') {
|
||||||
setCurrentPath(item.path)
|
onNavigate(item.path)
|
||||||
} else {
|
} else {
|
||||||
onOpenNote(item.path)
|
onOpenNote(item.path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onOpenNote, renameTarget],
|
[onNavigate, onOpenNote, renameTarget],
|
||||||
)
|
)
|
||||||
|
|
||||||
const beginRename = useCallback((item: TreeNode) => {
|
const beginRename = useCallback((item: TreeNode) => {
|
||||||
|
|
@ -295,7 +304,7 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
|
||||||
<div className="flex min-w-0 items-center gap-1 text-sm">
|
<div className="flex min-w-0 items-center gap-1 text-sm">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCurrentPath(WORKSPACE_ROOT)}
|
onClick={() => onNavigate(WORKSPACE_ROOT)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 transition-colors',
|
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 transition-colors',
|
||||||
isRoot ? 'text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent',
|
isRoot ? 'text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent',
|
||||||
|
|
@ -316,7 +325,7 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCurrentPath(crumb.path)}
|
onClick={() => onNavigate(crumb.path)}
|
||||||
className="rounded-md px-2 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground truncate"
|
className="rounded-md px-2 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground truncate"
|
||||||
>
|
>
|
||||||
{crumb.name}
|
{crumb.name}
|
||||||
|
|
@ -326,31 +335,42 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{isRoot ? (
|
<div className="grid shrink-0 grid-cols-2 items-center gap-2">
|
||||||
<Button size="sm" onClick={() => setAddOpen(true)}>
|
<Button
|
||||||
<Plus className="size-4" />
|
size="sm"
|
||||||
Add workspace
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => actions.revealInFileManager(currentPath, true)}
|
||||||
|
>
|
||||||
|
<FolderOpen className="size-4" />
|
||||||
|
Open in {fileManagerName}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
{isRoot ? (
|
||||||
<DropdownMenu>
|
<Button size="sm" className="w-full" onClick={() => setAddOpen(true)}>
|
||||||
<DropdownMenuTrigger asChild>
|
<Plus className="size-4" />
|
||||||
<Button size="sm">
|
Add workspace
|
||||||
<Plus className="size-4" />
|
</Button>
|
||||||
Add
|
) : (
|
||||||
</Button>
|
<DropdownMenu>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuContent align="end">
|
<Button size="sm" className="w-full">
|
||||||
<DropdownMenuItem onClick={() => filesInputRef.current?.click()}>
|
<Plus className="size-4" />
|
||||||
<FilePlus className="mr-2 size-4" />
|
Add
|
||||||
Add files…
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem onClick={() => folderInputRef.current?.click()}>
|
<DropdownMenuContent align="end">
|
||||||
<FolderPlus className="mr-2 size-4" />
|
<DropdownMenuItem onClick={() => filesInputRef.current?.click()}>
|
||||||
Add folder…
|
<FilePlus className="mr-2 size-4" />
|
||||||
</DropdownMenuItem>
|
Add files…
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem onClick={() => folderInputRef.current?.click()}>
|
||||||
)}
|
<FolderPlus className="mr-2 size-4" />
|
||||||
|
Add folder…
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={filesInputRef}
|
ref={filesInputRef}
|
||||||
|
|
@ -429,31 +449,56 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
|
||||||
) : (
|
) : (
|
||||||
<div className="truncate text-sm font-medium">{item.name}</div>
|
<div className="truncate text-sm font-medium">{item.name}</div>
|
||||||
)}
|
)}
|
||||||
{item.kind === 'dir' && !isRenaming && (
|
{!isRenaming && (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="truncate text-xs text-muted-foreground">
|
||||||
{childCount} {childCount === 1 ? 'item' : 'items'}
|
{item.kind === 'dir'
|
||||||
|
? `${childCount} ${childCount === 1 ? 'item' : 'items'}`
|
||||||
|
: fileExtensionLabel(item.name)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
const isDir = item.kind === 'dir'
|
||||||
return (
|
return (
|
||||||
<ContextMenu key={item.path}>
|
<ContextMenu key={item.path}>
|
||||||
<ContextMenuTrigger asChild>{card}</ContextMenuTrigger>
|
<ContextMenuTrigger asChild>{card}</ContextMenuTrigger>
|
||||||
<ContextMenuContent className="w-48">
|
<ContextMenuContent className="w-48" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
<ContextMenuItem onClick={() => beginRename(item)}>
|
{isDir && (
|
||||||
<Pencil className="mr-2 size-4" />
|
<>
|
||||||
Rename
|
<ContextMenuItem onClick={() => actions.createNote(item.path)}>
|
||||||
</ContextMenuItem>
|
<FilePlus className="mr-2 size-4" />
|
||||||
|
New Note
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={() => void actions.createFolder(item.path)}>
|
||||||
|
<FolderPlus className="mr-2 size-4" />
|
||||||
|
New Folder
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isDir && actions.onOpenInNewTab && (
|
||||||
|
<>
|
||||||
|
<ContextMenuItem onClick={() => actions.onOpenInNewTab!(item.path)}>
|
||||||
|
<ExternalLink className="mr-2 size-4" />
|
||||||
|
Open in new tab
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<ContextMenuItem onClick={() => { actions.copyPath(item.path); toast('Path copied', 'success') }}>
|
<ContextMenuItem onClick={() => { actions.copyPath(item.path); toast('Path copied', 'success') }}>
|
||||||
<Copy className="mr-2 size-4" />
|
<Copy className="mr-2 size-4" />
|
||||||
Copy Path
|
Copy Path
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem onClick={() => actions.revealInFileManager(item.path, item.kind === 'dir')}>
|
<ContextMenuItem onClick={() => actions.revealInFileManager(item.path, isDir)}>
|
||||||
<FolderOpen className="mr-2 size-4" />
|
<FolderOpen className="mr-2 size-4" />
|
||||||
Show in {fileManagerName}
|
Open in {fileManagerName}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem onClick={() => beginRename(item)}>
|
||||||
|
<Pencil className="mr-2 size-4" />
|
||||||
|
Rename
|
||||||
|
</ContextMenuItem>
|
||||||
<ContextMenuItem variant="destructive" onClick={() => void handleDelete(item)}>
|
<ContextMenuItem variant="destructive" onClick={() => void handleDelete(item)}>
|
||||||
<Trash2 className="mr-2 size-4" />
|
<Trash2 className="mr-2 size-4" />
|
||||||
Delete
|
Delete
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue