add right click to bases and fix copy path

This commit is contained in:
Arjun 2026-04-01 10:42:48 +05:30
parent 7a425acfdf
commit 54af172cbc
2 changed files with 146 additions and 18 deletions

View file

@ -3410,7 +3410,14 @@ function App() {
}, },
copyPath: (path: string) => { copyPath: (path: string) => {
const fullPath = workspaceRoot ? `${workspaceRoot}/${path}` : path const fullPath = workspaceRoot ? `${workspaceRoot}/${path}` : path
navigator.clipboard.writeText(fullPath) navigator.clipboard.writeText(fullPath).catch(() => {
const textarea = document.createElement('textarea')
textarea.value = fullPath
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
})
}, },
onOpenInNewTab: (path: string) => { onOpenInNewTab: (path: string) => {
openFileInNewTab(path) openFileInNewTab(path)
@ -4122,6 +4129,11 @@ function App() {
onSave={(name) => void handleBaseSave(name)} onSave={(name) => void handleBaseSave(name)}
externalSearch={externalBaseSearch} externalSearch={externalBaseSearch}
onExternalSearchConsumed={() => setExternalBaseSearch(undefined)} onExternalSearchConsumed={() => setExternalBaseSearch(undefined)}
actions={{
rename: knowledgeActions.rename,
remove: knowledgeActions.remove,
copyPath: knowledgeActions.copyPath,
}}
/> />
</div> </div>
) : isGraphOpen ? ( ) : isGraphOpen ? (

View file

@ -1,9 +1,16 @@
import * as React from 'react' import * as React from 'react'
import { useEffect, useState, useMemo, useCallback, useRef } from 'react' import { useEffect, useState, useMemo, useCallback, useRef } from 'react'
import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, X, Check, ListFilter, Filter, Search, Save } from 'lucide-react' import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, X, Check, ListFilter, Filter, Search, Save, ExternalLink, Copy, Pencil, Trash2 } from 'lucide-react'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import { Command, CommandInput, CommandList, CommandItem, CommandEmpty, CommandGroup } from '@/components/ui/command' import { Command, CommandInput, CommandList, CommandItem, CommandEmpty, CommandGroup } from '@/components/ui/command'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -91,6 +98,12 @@ type BasesViewProps = {
externalSearch?: string externalSearch?: string
/** Called after the external search has been consumed (applied to internal state). */ /** Called after the external search has been consumed (applied to internal state). */
onExternalSearchConsumed?: () => void onExternalSearchConsumed?: () => void
/** Actions for context menu */
actions?: {
rename: (oldPath: string, newName: string, isDir: boolean) => Promise<void>
remove: (path: string) => Promise<void>
copyPath: (path: string) => void
}
} }
function collectFiles(nodes: TreeNode[]): { path: string; name: string; mtimeMs: number }[] { function collectFiles(nodes: TreeNode[]): { path: string; name: string; mtimeMs: number }[] {
@ -143,7 +156,7 @@ function getSortValue(note: NoteEntry, column: string): string | number {
return Array.isArray(v) ? v[0] ?? '' : v return Array.isArray(v) ? v[0] ?? '' : v
} }
export function BasesView({ tree, onSelectNote, config, onConfigChange, isDefaultBase, onSave, externalSearch, onExternalSearchConsumed }: BasesViewProps) { export function BasesView({ tree, onSelectNote, config, onConfigChange, isDefaultBase, onSave, externalSearch, onExternalSearchConsumed, actions }: BasesViewProps) {
// Build notes instantly from tree // Build notes instantly from tree
const notes = useMemo<NoteEntry[]>(() => { const notes = useMemo<NoteEntry[]>(() => {
return collectFiles(tree).map((f) => ({ return collectFiles(tree).map((f) => ({
@ -652,22 +665,15 @@ export function BasesView({ tree, onSelectNote, config, onConfigChange, isDefaul
</thead> </thead>
<tbody> <tbody>
{pageNotes.map((note) => ( {pageNotes.map((note) => (
<tr <NoteRow
key={note.path} key={note.path}
className="border-b border-border/50 hover:bg-accent/50 cursor-pointer transition-colors" note={note}
onClick={() => onSelectNote(note.path)} visibleColumns={visibleColumns}
> filters={filters}
{visibleColumns.map((col) => ( toggleFilter={toggleFilter}
<td key={col} className="px-4 py-2 overflow-hidden"> onSelectNote={onSelectNote}
<CellRenderer actions={actions}
note={note} />
column={col}
filters={filters}
toggleFilter={toggleFilter}
/>
</td>
))}
</tr>
))} ))}
{pageNotes.length === 0 && ( {pageNotes.length === 0 && (
<tr> <tr>
@ -801,6 +807,116 @@ function CellRenderer({
) )
} }
function NoteRow({
note,
visibleColumns,
filters,
toggleFilter,
onSelectNote,
actions,
}: {
note: NoteEntry
visibleColumns: string[]
filters: ActiveFilter[]
toggleFilter: (category: string, value: string) => void
onSelectNote: (path: string) => void
actions?: BasesViewProps['actions']
}) {
const [isRenaming, setIsRenaming] = useState(false)
const [newName, setNewName] = useState('')
const isSubmittingRef = useRef(false)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (isRenaming) inputRef.current?.focus()
}, [isRenaming])
const baseName = note.name
const handleRenameSubmit = useCallback(async () => {
if (isSubmittingRef.current) return
const trimmed = newName.trim()
if (!trimmed || trimmed === baseName) {
setIsRenaming(false)
return
}
isSubmittingRef.current = true
try {
await actions?.rename(note.path, trimmed, false)
} catch {
// ignore
}
setIsRenaming(false)
isSubmittingRef.current = false
}, [newName, baseName, actions, note.path])
const handleCopyPath = useCallback(() => {
actions?.copyPath(note.path)
}, [actions, note.path])
const handleDelete = useCallback(() => {
void actions?.remove(note.path)
}, [actions, note.path])
const row = (
<tr
className="border-b border-border/50 hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => onSelectNote(note.path)}
>
{visibleColumns.map((col) => (
<td key={col} className="px-4 py-2 overflow-hidden">
{col === 'name' && isRenaming ? (
<input
ref={inputRef}
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
onBlur={() => void handleRenameSubmit()}
onKeyDown={(e) => {
if (e.key === 'Enter') void handleRenameSubmit()
if (e.key === 'Escape') setIsRenaming(false)
}}
onClick={(e) => e.stopPropagation()}
className="w-full bg-transparent text-sm font-medium outline-none ring-1 ring-ring rounded px-1"
/>
) : (
<CellRenderer
note={note}
column={col}
filters={filters}
toggleFilter={toggleFilter}
/>
)}
</td>
))}
</tr>
)
if (!actions) return row
return (
<ContextMenu>
<ContextMenuTrigger asChild>
{row}
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuItem onClick={handleCopyPath}>
<Copy className="mr-2 size-4" />
Copy Path
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => { setNewName(baseName); isSubmittingRef.current = false; setIsRenaming(true) }}>
<Pencil className="mr-2 size-4" />
Rename
</ContextMenuItem>
<ContextMenuItem variant="destructive" onClick={handleDelete}>
<Trash2 className="mr-2 size-4" />
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
function CategoryBadge({ function CategoryBadge({
category, category,
value, value,