mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
workspace side bar
This commit is contained in:
parent
43537c5e20
commit
e678dd71a0
1 changed files with 18 additions and 166 deletions
|
|
@ -10,13 +10,11 @@ import {
|
||||||
FileText,
|
FileText,
|
||||||
FilePlus,
|
FilePlus,
|
||||||
Folder,
|
Folder,
|
||||||
FolderOpen,
|
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
Globe,
|
Globe,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Mic,
|
Mic,
|
||||||
LayoutGrid,
|
|
||||||
Plug,
|
Plug,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
|
|
@ -76,7 +74,6 @@ import {
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { ConnectorsPopover } from "@/components/connectors-popover"
|
import { ConnectorsPopover } from "@/components/connectors-popover"
|
||||||
import { HelpPopover } from "@/components/help-popover"
|
import { HelpPopover } from "@/components/help-popover"
|
||||||
|
|
@ -120,14 +117,6 @@ function displayNoteName(node: TreeNode): string {
|
||||||
return node.name
|
return node.name
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileManagerName(): string {
|
|
||||||
if (typeof navigator === 'undefined') return 'File Manager'
|
|
||||||
const platform = navigator.platform.toLowerCase()
|
|
||||||
if (platform.includes('mac')) return 'Finder'
|
|
||||||
if (platform.includes('win')) return 'Explorer'
|
|
||||||
return 'File Manager'
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunListItem = {
|
type RunListItem = {
|
||||||
id: string
|
id: string
|
||||||
title?: string
|
title?: string
|
||||||
|
|
@ -1097,12 +1086,13 @@ export function WorkspaceSection({
|
||||||
tree: TreeNode[]
|
tree: TreeNode[]
|
||||||
actions: KnowledgeActions
|
actions: KnowledgeActions
|
||||||
}) {
|
}) {
|
||||||
const workspaceFolders = React.useMemo<TreeNode[]>(() => {
|
const recentWorkspaces = React.useMemo<TreeNode[]>(() => {
|
||||||
const root = tree.find((item) => item.path === 'knowledge/Workspace')
|
const root = tree.find((item) => item.path === 'knowledge/Workspace')
|
||||||
const children = root?.children ?? []
|
const children = root?.children ?? []
|
||||||
return [...children]
|
return [...children]
|
||||||
.filter((c) => c.kind === 'dir')
|
.filter((c) => c.kind === 'dir')
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => (b.stat?.mtimeMs ?? 0) - (a.stat?.mtimeMs ?? 0))
|
||||||
|
.slice(0, 3)
|
||||||
}, [tree])
|
}, [tree])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1110,163 +1100,25 @@ export function WorkspaceSection({
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
Workspace
|
Workspace
|
||||||
</div>
|
</div>
|
||||||
<WorkspacesPanel
|
|
||||||
workspaces={workspaceFolders}
|
|
||||||
onOpen={(path) => actions.openWorkspaceAt(path)}
|
|
||||||
onOpenRoot={() => actions.openWorkspaceAt()}
|
|
||||||
onCreate={actions.createWorkspace}
|
|
||||||
onRevealInFileManager={() => actions.revealInFileManager('knowledge/Workspace', true)}
|
|
||||||
/>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkspacesPanel({
|
|
||||||
workspaces,
|
|
||||||
onOpen,
|
|
||||||
onOpenRoot,
|
|
||||||
onCreate,
|
|
||||||
onRevealInFileManager,
|
|
||||||
}: {
|
|
||||||
workspaces: TreeNode[]
|
|
||||||
onOpen: (path: string) => void
|
|
||||||
onOpenRoot: () => void
|
|
||||||
onCreate: (name: string) => Promise<string>
|
|
||||||
onRevealInFileManager: () => void
|
|
||||||
}) {
|
|
||||||
const fileManagerName = getFileManagerName()
|
|
||||||
const [addOpen, setAddOpen] = useState(false)
|
|
||||||
const [newName, setNewName] = useState('')
|
|
||||||
const [creating, setCreating] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const resetDialog = useCallback(() => {
|
|
||||||
setNewName('')
|
|
||||||
setError(null)
|
|
||||||
setCreating(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleCreate = useCallback(async () => {
|
|
||||||
setCreating(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const newPath = await onCreate(newName)
|
|
||||||
setAddOpen(false)
|
|
||||||
resetDialog()
|
|
||||||
onOpen(newPath)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to create workspace')
|
|
||||||
setCreating(false)
|
|
||||||
}
|
|
||||||
}, [newName, onCreate, onOpen, resetDialog])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-center gap-1 py-1 sticky top-0 z-10 bg-sidebar border-b border-sidebar-border">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAddOpen(true)}
|
|
||||||
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
|
|
||||||
>
|
|
||||||
<FolderPlus className="size-4" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">Add workspace</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onOpenRoot}
|
|
||||||
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
|
|
||||||
>
|
|
||||||
<LayoutGrid className="size-4" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">Workspace view</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRevealInFileManager}
|
|
||||||
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
|
|
||||||
>
|
|
||||||
<FolderOpen className="size-4" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">Open in {fileManagerName}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{workspaces.length === 0 ? (
|
{recentWorkspaces.map((ws) => (
|
||||||
<div className="px-3 py-2 text-xs text-sidebar-foreground/60">
|
<SidebarMenuItem key={ws.path}>
|
||||||
No workspaces yet.
|
<SidebarMenuButton onClick={() => actions.openWorkspaceAt(ws.path)}>
|
||||||
</div>
|
<Folder className="size-4 shrink-0" />
|
||||||
) : (
|
<span className="truncate">{ws.name}</span>
|
||||||
workspaces.map((ws) => (
|
</SidebarMenuButton>
|
||||||
<SidebarMenuItem key={ws.path}>
|
</SidebarMenuItem>
|
||||||
<SidebarMenuButton onClick={() => onOpen(ws.path)}>
|
))}
|
||||||
<Folder className="size-4 shrink-0" />
|
<SidebarMenuItem>
|
||||||
<span className="truncate">{ws.name}</span>
|
<SidebarMenuButton onClick={() => actions.openWorkspaceAt()}>
|
||||||
</SidebarMenuButton>
|
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||||
</SidebarMenuItem>
|
<span className="text-muted-foreground">View all</span>
|
||||||
))
|
</SidebarMenuButton>
|
||||||
)}
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
<Dialog
|
</SidebarGroup>
|
||||||
open={addOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setAddOpen(open)
|
|
||||||
if (!open) resetDialog()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>New workspace</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Workspaces are top-level folders inside knowledge/Workspace.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<label htmlFor="sidebar-workspace-name" className="text-sm font-medium">Name</label>
|
|
||||||
<Input
|
|
||||||
id="sidebar-workspace-name"
|
|
||||||
value={newName}
|
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
|
||||||
placeholder="e.g. Alpha"
|
|
||||||
autoFocus
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !creating) {
|
|
||||||
e.preventDefault()
|
|
||||||
void handleCreate()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setAddOpen(false)
|
|
||||||
resetDialog()
|
|
||||||
}}
|
|
||||||
disabled={creating}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => void handleCreate()} disabled={creating || !newName.trim()}>
|
|
||||||
{creating ? 'Creating…' : 'Create'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue