mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Generated by Spark: Add bulk move feature to move multiple snippets at once
This commit is contained in:
@@ -2,6 +2,7 @@ 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 { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -25,9 +26,22 @@ interface SnippetCardProps {
|
||||
onCopy: (code: string) => void
|
||||
onView: (snippet: Snippet) => void
|
||||
onMove?: () => void
|
||||
selectionMode?: boolean
|
||||
isSelected?: boolean
|
||||
onToggleSelect?: (id: string) => void
|
||||
}
|
||||
|
||||
export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView, onMove }: SnippetCardProps) {
|
||||
export function SnippetCard({
|
||||
snippet,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCopy,
|
||||
onView,
|
||||
onMove,
|
||||
selectionMode = false,
|
||||
isSelected = false,
|
||||
onToggleSelect
|
||||
}: SnippetCardProps) {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const [namespaces, setNamespaces] = useState<Namespace[]>([])
|
||||
const [isMoving, setIsMoving] = useState(false)
|
||||
@@ -89,7 +103,17 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView, onMove
|
||||
}
|
||||
|
||||
const handleView = (e: React.MouseEvent) => {
|
||||
onView(snippet)
|
||||
if (selectionMode) {
|
||||
handleToggleSelect()
|
||||
} else {
|
||||
onView(snippet)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleSelect = () => {
|
||||
if (onToggleSelect) {
|
||||
onToggleSelect(snippet.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMoveToNamespace = async (targetNamespaceId: string) => {
|
||||
@@ -127,20 +151,32 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView, onMove
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="group overflow-hidden hover:border-accent/50 transition-all cursor-pointer"
|
||||
className={`group overflow-hidden hover:border-accent/50 transition-all cursor-pointer ${
|
||||
isSelected ? 'border-accent ring-2 ring-accent/20' : ''
|
||||
}`}
|
||||
onClick={handleView}
|
||||
>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1 truncate">
|
||||
{snippet.title}
|
||||
</h3>
|
||||
{snippetData.description && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{snippetData.description}
|
||||
</p>
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
{selectionMode && (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={handleToggleSelect}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1 truncate">
|
||||
{snippet.title}
|
||||
</h3>
|
||||
{snippetData.description && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{snippetData.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className={`shrink-0 ${LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']}`}
|
||||
@@ -160,87 +196,89 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView, onMove
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleView}
|
||||
className="gap-2"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
{strings.snippetCard.viewButton}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="gap-2"
|
||||
aria-label={strings.snippetCard.ariaLabels.copy}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
{isCopied ? strings.snippetCard.copiedButton : strings.snippetCard.copyButton}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleEdit}
|
||||
aria-label={strings.snippetCard.ariaLabels.edit}
|
||||
>
|
||||
<Pencil 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>
|
||||
)}
|
||||
{!selectionMode && (
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleView}
|
||||
className="gap-2"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
{strings.snippetCard.viewButton}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="gap-2"
|
||||
aria-label={strings.snippetCard.ariaLabels.copy}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
{isCopied ? strings.snippetCard.copiedButton : strings.snippetCard.copyButton}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleEdit}
|
||||
aria-label={strings.snippetCard.ariaLabels.edit}
|
||||
>
|
||||
<Pencil 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>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={handleDelete}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Plus, MagnifyingGlass, CaretDown } from '@phosphor-icons/react'
|
||||
import { Plus, MagnifyingGlass, CaretDown, CheckSquare, FolderOpen, X } from '@phosphor-icons/react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -9,13 +9,16 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { SnippetCard } from '@/components/SnippetCard'
|
||||
import { SnippetDialog } from '@/components/SnippetDialog'
|
||||
import { SnippetViewer } from '@/components/SnippetViewer'
|
||||
import { EmptyState } from '@/components/EmptyState'
|
||||
import { NamespaceSelector } from '@/components/NamespaceSelector'
|
||||
import { Snippet, SnippetTemplate } from '@/lib/types'
|
||||
import { Snippet, SnippetTemplate, Namespace } from '@/lib/types'
|
||||
import { toast } from 'sonner'
|
||||
import { strings } from '@/lib/config'
|
||||
import templatesData from '@/data/templates.json'
|
||||
@@ -28,7 +31,8 @@ import {
|
||||
syncTemplatesFromJSON,
|
||||
getSnippetsByNamespace,
|
||||
ensureDefaultNamespace,
|
||||
getAllNamespaces
|
||||
getAllNamespaces,
|
||||
bulkMoveSnippets
|
||||
} from '@/lib/db'
|
||||
|
||||
const templates = templatesData as SnippetTemplate[]
|
||||
@@ -42,6 +46,9 @@ export function SnippetManager() {
|
||||
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null)
|
||||
const [viewingSnippet, setViewingSnippet] = useState<Snippet | null>(null)
|
||||
const [selectedNamespaceId, setSelectedNamespaceId] = useState<string | null>(null)
|
||||
const [selectionMode, setSelectionMode] = useState(false)
|
||||
const [selectedSnippetIds, setSelectedSnippetIds] = useState<Set<string>>(new Set())
|
||||
const [namespaces, setNamespaces] = useState<Namespace[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
@@ -50,8 +57,10 @@ export function SnippetManager() {
|
||||
await seedDatabase()
|
||||
await syncTemplatesFromJSON(templates)
|
||||
|
||||
const namespaces = await getAllNamespaces()
|
||||
const defaultNamespace = namespaces.find(n => n.isDefault)
|
||||
const loadedNamespaces = await getAllNamespaces()
|
||||
setNamespaces(loadedNamespaces)
|
||||
|
||||
const defaultNamespace = loadedNamespaces.find(n => n.isDefault)
|
||||
if (defaultNamespace) {
|
||||
setSelectedNamespaceId(defaultNamespace.id)
|
||||
const loadedSnippets = await getSnippetsByNamespace(defaultNamespace.id)
|
||||
@@ -198,6 +207,55 @@ export function SnippetManager() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleToggleSelectionMode = useCallback(() => {
|
||||
setSelectionMode(!selectionMode)
|
||||
setSelectedSnippetIds(new Set())
|
||||
}, [selectionMode])
|
||||
|
||||
const handleToggleSnippetSelection = useCallback((snippetId: string) => {
|
||||
setSelectedSnippetIds((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(snippetId)) {
|
||||
newSet.delete(snippetId)
|
||||
} else {
|
||||
newSet.add(snippetId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (selectedSnippetIds.size === filteredSnippets.length) {
|
||||
setSelectedSnippetIds(new Set())
|
||||
} else {
|
||||
setSelectedSnippetIds(new Set(filteredSnippets.map(s => s.id)))
|
||||
}
|
||||
}, [filteredSnippets, selectedSnippetIds.size])
|
||||
|
||||
const handleBulkMove = useCallback(async (targetNamespaceId: string) => {
|
||||
if (selectedSnippetIds.size === 0) {
|
||||
toast.error('No snippets selected')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await bulkMoveSnippets(Array.from(selectedSnippetIds), targetNamespaceId)
|
||||
const targetNamespace = namespaces.find(n => n.id === targetNamespaceId)
|
||||
toast.success(`Moved ${selectedSnippetIds.size} snippet${selectedSnippetIds.size > 1 ? 's' : ''} to ${targetNamespace?.name || 'namespace'}`)
|
||||
|
||||
setSelectedSnippetIds(new Set())
|
||||
setSelectionMode(false)
|
||||
|
||||
if (selectedNamespaceId) {
|
||||
const loadedSnippets = await getSnippetsByNamespace(selectedNamespaceId)
|
||||
setSnippets(loadedSnippets)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to bulk move snippets:', error)
|
||||
toast.error('Failed to move snippets')
|
||||
}
|
||||
}, [selectedSnippetIds, namespaces, selectedNamespaceId])
|
||||
|
||||
const allSnippets = snippets || []
|
||||
|
||||
if (loading) {
|
||||
@@ -248,113 +306,180 @@ export function SnippetManager() {
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="gap-2 w-full sm:w-auto">
|
||||
<Plus weight="bold" />
|
||||
{strings.app.header.newSnippetButton}
|
||||
<CaretDown weight="bold" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-72 max-h-[500px] overflow-y-auto">
|
||||
<DropdownMenuItem onClick={handleCreateNew}>
|
||||
<Plus className="mr-2 h-4 w-4" weight="bold" />
|
||||
Blank Snippet
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>React Components</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'react').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 w-full sm:w-auto">
|
||||
<Button
|
||||
variant={selectionMode ? "default" : "outline"}
|
||||
onClick={handleToggleSelectionMode}
|
||||
className="gap-2"
|
||||
>
|
||||
{selectionMode ? (
|
||||
<>
|
||||
<X weight="bold" />
|
||||
Cancel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckSquare weight="bold" />
|
||||
Select
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="gap-2 w-full sm:w-auto">
|
||||
<Plus weight="bold" />
|
||||
{strings.app.header.newSnippetButton}
|
||||
<CaretDown weight="bold" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-72 max-h-[500px] overflow-y-auto">
|
||||
<DropdownMenuItem onClick={handleCreateNew}>
|
||||
<Plus className="mr-2 h-4 w-4" weight="bold" />
|
||||
Blank Snippet
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>JavaScript / TypeScript</DropdownMenuLabel>
|
||||
{templates.filter((t) => ['api', 'basics', 'async', 'types'].includes(t.category)).map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>CSS Layouts</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'layout').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Python - Project Euler</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'euler').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Python - Algorithms</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'algorithms' && t.language === 'Python').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Python - Interactive Programs</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'interactive').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>React Components</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'react').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>JavaScript / TypeScript</DropdownMenuLabel>
|
||||
{templates.filter((t) => ['api', 'basics', 'async', 'types'].includes(t.category)).map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>CSS Layouts</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'layout').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Python - Project Euler</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'euler').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Python - Algorithms</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'algorithms' && t.language === 'Python').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Python - Interactive Programs</DropdownMenuLabel>
|
||||
{templates.filter((t) => t.category === 'interactive').map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onClick={() => handleCreateFromTemplate(template.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
<span className="font-medium">{template.title}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{template.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectionMode && (
|
||||
<div className="flex items-center justify-between gap-4 p-4 bg-accent/10 border border-accent/30 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
{selectedSnippetIds.size === filteredSnippets.length ? 'Deselect All' : 'Select All'}
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{selectedSnippetIds.size} of {filteredSnippets.length} selected
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
disabled={selectedSnippetIds.size === 0}
|
||||
className="gap-2"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
Move to...
|
||||
<CaretDown weight="bold" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{namespaces
|
||||
.filter(n => n.id !== selectedNamespaceId)
|
||||
.map((namespace) => (
|
||||
<DropdownMenuItem
|
||||
key={namespace.id}
|
||||
onClick={() => handleBulkMove(namespace.id)}
|
||||
>
|
||||
{namespace.name}
|
||||
{namespace.isDefault && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">(Default)</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredSnippets.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<h3 className="text-2xl font-semibold mb-2">{strings.noResults.title}</h3>
|
||||
@@ -371,6 +496,9 @@ export function SnippetManager() {
|
||||
onCopy={handleCopyCode}
|
||||
onView={handleViewSnippet}
|
||||
onMove={handleMoveSnippet}
|
||||
selectionMode={selectionMode}
|
||||
isSelected={selectedSnippetIds.has(snippet.id)}
|
||||
onToggleSelect={handleToggleSnippetSelection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1124,6 +1124,26 @@ export async function moveSnippetToNamespace(snippetId: string, targetNamespaceI
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
await adapter.bulkMoveSnippets(snippetIds, targetNamespaceId)
|
||||
return
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const now = Date.now()
|
||||
|
||||
for (const snippetId of snippetIds) {
|
||||
db.run(
|
||||
'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
|
||||
[targetNamespaceId, now, snippetId]
|
||||
)
|
||||
}
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function validateDatabaseSchema(): Promise<{ valid: boolean; issues: string[] }> {
|
||||
try {
|
||||
const db = await initDB()
|
||||
|
||||
@@ -235,4 +235,18 @@ export class FlaskStorageAdapter {
|
||||
throw new Error(`Failed to wipe database: ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
async bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise<void> {
|
||||
if (!this.isValidUrl()) {
|
||||
throw new Error('Invalid Flask backend URL')
|
||||
}
|
||||
const response = await fetch(`${this.baseUrl}/api/snippets/bulk-move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ snippetIds, targetNamespaceId })
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to bulk move snippets: ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user