mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-01 19:32:40 +02:00
add right click to bases and fix copy path
This commit is contained in:
parent
7a425acfdf
commit
54af172cbc
2 changed files with 146 additions and 18 deletions
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue