Generated by Spark: Make it easy to move a card to another namespace

This commit is contained in:
2026-01-17 20:11:05 +00:00
committed by GitHub
parent 18e211b774
commit 735ca6c633
3 changed files with 140 additions and 13 deletions

View File

@@ -1,10 +1,22 @@
import { useState, useMemo } from 'react'
import { useState, useMemo, useEffect } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Copy, Pencil, Trash, Eye } from '@phosphor-icons/react'
import { Snippet } from '@/lib/types'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Copy, Pencil, Trash, Eye, DotsThree, FolderOpen } from '@phosphor-icons/react'
import { Snippet, Namespace } from '@/lib/types'
import { strings, appConfig, LANGUAGE_COLORS } from '@/lib/config'
import { getAllNamespaces, moveSnippetToNamespace } from '@/lib/db'
import { toast } from 'sonner'
interface SnippetCardProps {
snippet: Snippet
@@ -12,10 +24,26 @@ interface SnippetCardProps {
onDelete: (id: string) => void
onCopy: (code: string) => void
onView: (snippet: Snippet) => void
onMove?: () => void
}
export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: SnippetCardProps) {
export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView, onMove }: SnippetCardProps) {
const [isCopied, setIsCopied] = useState(false)
const [namespaces, setNamespaces] = useState<Namespace[]>([])
const [isMoving, setIsMoving] = useState(false)
useEffect(() => {
loadNamespaces()
}, [])
const loadNamespaces = async () => {
try {
const loadedNamespaces = await getAllNamespaces()
setNamespaces(loadedNamespaces)
} catch (error) {
console.error('Failed to load namespaces:', error)
}
}
const snippetData = useMemo(() => {
try {
@@ -64,6 +92,28 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp
onView(snippet)
}
const handleMoveToNamespace = async (targetNamespaceId: string) => {
if (snippet.namespaceId === targetNamespaceId) {
toast.info('Snippet is already in this namespace')
return
}
setIsMoving(true)
try {
await moveSnippetToNamespace(snippet.id, targetNamespaceId)
const targetNamespace = namespaces.find(n => n.id === targetNamespaceId)
toast.success(`Moved to ${targetNamespace?.name || 'namespace'}`)
if (onMove) {
onMove()
}
} catch (error) {
console.error('Failed to move snippet:', error)
toast.error('Failed to move snippet')
} finally {
setIsMoving(false)
}
}
if (!snippet) {
return (
<Card className="p-6">
@@ -72,6 +122,9 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp
)
}
const currentNamespace = namespaces.find(n => n.id === snippet.namespaceId)
const availableNamespaces = namespaces.filter(n => n.id !== snippet.namespaceId)
return (
<Card
className="group overflow-hidden hover:border-accent/50 transition-all cursor-pointer"
@@ -138,15 +191,54 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleDelete}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
aria-label={strings.snippetCard.ariaLabels.delete}
>
<Trash className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => e.stopPropagation()}
aria-label="More options"
>
<DotsThree className="h-4 w-4" weight="bold" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuSub>
<DropdownMenuSubTrigger disabled={isMoving || availableNamespaces.length === 0}>
<FolderOpen className="h-4 w-4 mr-2" />
<span>Move to...</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{availableNamespaces.length === 0 ? (
<DropdownMenuItem disabled>
No other namespaces
</DropdownMenuItem>
) : (
availableNamespaces.map((namespace) => (
<DropdownMenuItem
key={namespace.id}
onClick={() => handleMoveToNamespace(namespace.id)}
>
{namespace.name}
{namespace.isDefault && (
<span className="ml-2 text-xs text-muted-foreground">(Default)</span>
)}
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive focus:text-destructive"
>
<Trash className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>

View File

@@ -156,6 +156,18 @@ export function SnippetManager() {
setViewerOpen(true)
}, [])
const handleMoveSnippet = useCallback(async () => {
if (!selectedNamespaceId) return
try {
const loadedSnippets = await getSnippetsByNamespace(selectedNamespaceId)
setSnippets(loadedSnippets)
} catch (error) {
console.error('Failed to reload snippets:', error)
toast.error('Failed to reload snippets')
}
}, [selectedNamespaceId])
const handleCreateNew = useCallback(() => {
setEditingSnippet(null)
setDialogOpen(true)
@@ -358,6 +370,7 @@ export function SnippetManager() {
onDelete={handleDeleteSnippet}
onCopy={handleCopyCode}
onView={handleViewSnippet}
onMove={handleMoveSnippet}
/>
))}
</div>

View File

@@ -1102,6 +1102,28 @@ export async function getNamespaceById(id: string): Promise<import('./types').Na
return namespace
}
export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise<void> {
const adapter = getFlaskAdapter()
if (adapter) {
const snippet = await adapter.getSnippet(snippetId)
if (snippet) {
snippet.namespaceId = targetNamespaceId
snippet.updatedAt = Date.now()
await adapter.updateSnippet(snippet)
}
return
}
const db = await initDB()
db.run(
'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
[targetNamespaceId, Date.now(), snippetId]
)
await saveDB()
}
export async function validateDatabaseSchema(): Promise<{ valid: boolean; issues: string[] }> {
try {
const db = await initDB()