From 54af172cbc11a1db4fd22c4e62b5ec63b92015f0 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:42:48 +0530 Subject: [PATCH] add right click to bases and fix copy path --- apps/x/apps/renderer/src/App.tsx | 14 +- .../renderer/src/components/bases-view.tsx | 150 ++++++++++++++++-- 2 files changed, 146 insertions(+), 18 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 8c20dd12..fe2848ef 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3410,7 +3410,14 @@ function App() { }, copyPath: (path: string) => { 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) => { openFileInNewTab(path) @@ -4122,6 +4129,11 @@ function App() { onSave={(name) => void handleBaseSave(name)} externalSearch={externalBaseSearch} onExternalSearchConsumed={() => setExternalBaseSearch(undefined)} + actions={{ + rename: knowledgeActions.rename, + remove: knowledgeActions.remove, + copyPath: knowledgeActions.copyPath, + }} /> ) : isGraphOpen ? ( diff --git a/apps/x/apps/renderer/src/components/bases-view.tsx b/apps/x/apps/renderer/src/components/bases-view.tsx index cad6ccbf..6eab6853 100644 --- a/apps/x/apps/renderer/src/components/bases-view.tsx +++ b/apps/x/apps/renderer/src/components/bases-view.tsx @@ -1,9 +1,16 @@ import * as React 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 { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' import { Command, CommandInput, CommandList, CommandItem, CommandEmpty, CommandGroup } from '@/components/ui/command' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from '@/components/ui/context-menu' import { Dialog, DialogContent, @@ -91,6 +98,12 @@ type BasesViewProps = { externalSearch?: string /** Called after the external search has been consumed (applied to internal state). */ onExternalSearchConsumed?: () => void + /** Actions for context menu */ + actions?: { + rename: (oldPath: string, newName: string, isDir: boolean) => Promise + remove: (path: string) => Promise + copyPath: (path: string) => void + } } 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 } -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 const notes = useMemo(() => { return collectFiles(tree).map((f) => ({ @@ -652,22 +665,15 @@ export function BasesView({ tree, onSelectNote, config, onConfigChange, isDefaul {pageNotes.map((note) => ( - onSelectNote(note.path)} - > - {visibleColumns.map((col) => ( - - - - ))} - + note={note} + visibleColumns={visibleColumns} + filters={filters} + toggleFilter={toggleFilter} + onSelectNote={onSelectNote} + actions={actions} + /> ))} {pageNotes.length === 0 && ( @@ -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(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 = ( + onSelectNote(note.path)} + > + {visibleColumns.map((col) => ( + + {col === 'name' && isRenaming ? ( + 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" + /> + ) : ( + + )} + + ))} + + ) + + if (!actions) return row + + return ( + + + {row} + + + + + Copy Path + + + { setNewName(baseName); isSubmittingRef.current = false; setIsRenaming(true) }}> + + Rename + + + + Delete + + + + ) +} + function CategoryBadge({ category, value,