mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Generated by Spark: Would be nice to group snippets with namespaces. Allow user to create / delete unlimited namespaces. Reserve 1 default namespace. If user deletes a namespaces, it could move all the cards back to the default one.
This commit is contained in:
29
PRD.md
29
PRD.md
@@ -39,11 +39,18 @@ A code snippet management application with an integrated component library showc
|
||||
- Progression: User enables preview → Split editor appears → User types code → Preview/output updates in real-time → For Python programs with input(), user enters values in interactive prompts → User adjusts panel sizes or switches view modes → Saves snippet
|
||||
- Success criteria: React preview updates within 100ms of code changes, Python code executes reliably using Pyodide (WebAssembly Python), interactive input() prompts work seamlessly with multiple inputs, no lag during typing, error messages display clearly with AI help option
|
||||
|
||||
**Snippet Organization**
|
||||
- Functionality: Real-time search across title, description, language, and code content
|
||||
- Purpose: Easy management of growing snippet collections
|
||||
- Progression: User types in search → Results filter instantly → User finds desired snippet
|
||||
- Success criteria: Search responds within 100ms, filters accurately
|
||||
**Snippet Organization**
|
||||
- Functionality: Real-time search across title, description, language, and code content, plus namespace-based organization with unlimited custom namespaces
|
||||
- Purpose: Easy management of growing snippet collections with flexible categorization
|
||||
- Progression: User types in search → Results filter instantly → User finds desired snippet, or User selects namespace from dropdown → Only snippets in that namespace display → User creates new namespace via + button → User can delete non-default namespaces (snippets move to Default)
|
||||
- Success criteria: Search responds within 100ms, filters accurately, namespace switching is instant, creating/deleting namespaces works smoothly, default namespace cannot be deleted
|
||||
|
||||
**Namespace Management**
|
||||
- Functionality: Create, switch between, and delete custom namespaces to organize snippets into logical groups
|
||||
- Purpose: Enable users to organize large snippet collections by project, language, or any custom category
|
||||
- Trigger: Click namespace dropdown or + button in the namespace selector
|
||||
- Progression: User clicks namespace dropdown → Sees all available namespaces (Default first) → Selects a namespace → Snippets filtered to that namespace only → User clicks + button → Enters namespace name → New namespace created → User can switch to it → User can delete any non-default namespace via trash icon → Confirmation dialog appears → Snippets in deleted namespace move to Default
|
||||
- Success criteria: Default namespace always exists and cannot be deleted, unlimited namespaces can be created, deleting a namespace moves all its snippets to Default, namespace selection persists during session, new snippets automatically assigned to current namespace
|
||||
|
||||
**Component Library Pages**
|
||||
- Functionality: Separate pages for Atoms, Molecules, Organisms, and Templates
|
||||
@@ -72,12 +79,12 @@ The application supports **flexible data storage** with two backend options:
|
||||
|
||||
### Storage Backends
|
||||
|
||||
1. **IndexedDB (Local Browser Storage) - Default**
|
||||
- Uses SQL.js (SQLite compiled to WebAssembly) for local database management
|
||||
- Primary Storage: IndexedDB - Used when available for better performance and larger storage capacity (typically 50MB+)
|
||||
- Fallback: localStorage - Used when IndexedDB is unavailable (typically 5-10MB limit)
|
||||
- Database Structure: Two tables - `snippets` (user-created snippets) and `snippet_templates` (reusable templates)
|
||||
- Automatic Persistence: Database is automatically saved after every create, update, or delete operation
|
||||
1. **IndexedDB (Local Browser Storage) - Default**
|
||||
- Uses SQL.js (SQLite compiled to WebAssembly) for local database management
|
||||
- Primary Storage: IndexedDB - Used when available for better performance and larger storage capacity (typically 50MB+)
|
||||
- Fallback: localStorage - Used when IndexedDB is unavailable (typically 5-10MB limit)
|
||||
- Database Structure: Three tables - `snippets` (user-created snippets with namespaceId foreign key), `snippet_templates` (reusable templates), and `namespaces` (custom organizational categories with one default namespace)
|
||||
- Automatic Persistence: Database is automatically saved after every create, update, or delete operation
|
||||
- Export/Import: Users can export their entire database as a .db file for backup or transfer
|
||||
|
||||
2. **Flask Backend (Remote Server) - Optional**
|
||||
|
||||
215
src/components/NamespaceSelector.tsx
Normal file
215
src/components/NamespaceSelector.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Plus, Trash, Folder } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { Namespace } from '@/lib/types'
|
||||
import {
|
||||
getAllNamespaces,
|
||||
createNamespace,
|
||||
deleteNamespace,
|
||||
} from '@/lib/db'
|
||||
|
||||
interface NamespaceSelectorProps {
|
||||
selectedNamespaceId: string | null
|
||||
onNamespaceChange: (namespaceId: string) => void
|
||||
}
|
||||
|
||||
export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: NamespaceSelectorProps) {
|
||||
const [namespaces, setNamespaces] = useState<Namespace[]>([])
|
||||
const [newNamespaceName, setNewNamespaceName] = useState('')
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [namespaceToDelete, setNamespaceToDelete] = useState<Namespace | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadNamespaces()
|
||||
}, [])
|
||||
|
||||
const loadNamespaces = async () => {
|
||||
try {
|
||||
const loadedNamespaces = await getAllNamespaces()
|
||||
setNamespaces(loadedNamespaces)
|
||||
|
||||
if (!selectedNamespaceId && loadedNamespaces.length > 0) {
|
||||
const defaultNamespace = loadedNamespaces.find(n => n.isDefault)
|
||||
if (defaultNamespace) {
|
||||
onNamespaceChange(defaultNamespace.id)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load namespaces:', error)
|
||||
toast.error('Failed to load namespaces')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateNamespace = async () => {
|
||||
if (!newNamespaceName.trim()) {
|
||||
toast.error('Please enter a namespace name')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const newNamespace = await createNamespace(newNamespaceName.trim())
|
||||
setNamespaces(prev => [...prev, newNamespace])
|
||||
setNewNamespaceName('')
|
||||
setCreateDialogOpen(false)
|
||||
toast.success('Namespace created')
|
||||
} catch (error) {
|
||||
console.error('Failed to create namespace:', error)
|
||||
toast.error('Failed to create namespace')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteNamespace = async () => {
|
||||
if (!namespaceToDelete) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await deleteNamespace(namespaceToDelete.id)
|
||||
setNamespaces(prev => prev.filter(n => n.id !== namespaceToDelete.id))
|
||||
|
||||
if (selectedNamespaceId === namespaceToDelete.id) {
|
||||
const defaultNamespace = namespaces.find(n => n.isDefault)
|
||||
if (defaultNamespace) {
|
||||
onNamespaceChange(defaultNamespace.id)
|
||||
}
|
||||
}
|
||||
|
||||
setDeleteDialogOpen(false)
|
||||
setNamespaceToDelete(null)
|
||||
toast.success('Namespace deleted, snippets moved to default')
|
||||
} catch (error) {
|
||||
console.error('Failed to delete namespace:', error)
|
||||
toast.error('Failed to delete namespace')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedNamespace = namespaces.find(n => n.id === selectedNamespaceId)
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Folder weight="fill" className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Namespace:</span>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={selectedNamespaceId || undefined}
|
||||
onValueChange={onNamespaceChange}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select namespace">
|
||||
{selectedNamespace?.name || 'Select namespace'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{namespaces.map(namespace => (
|
||||
<SelectItem key={namespace.id} value={namespace.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{namespace.name}</span>
|
||||
{namespace.isDefault && (
|
||||
<span className="text-xs text-muted-foreground">(Default)</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Plus weight="bold" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Namespace</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new namespace to organize your snippets
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Input
|
||||
placeholder="Namespace name"
|
||||
value={newNamespaceName}
|
||||
onChange={(e) => setNewNamespaceName(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleCreateNamespace()}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateNamespace} disabled={loading}>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{selectedNamespace && !selectedNamespace.isDefault && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setNamespaceToDelete(selectedNamespace)
|
||||
setDeleteDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Trash weight="bold" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Namespace</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{namespaceToDelete?.name}"? All snippets in this namespace will be moved to the default namespace.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteNamespace} disabled={loading}>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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 { toast } from 'sonner'
|
||||
import { strings } from '@/lib/config'
|
||||
@@ -24,7 +25,10 @@ import {
|
||||
updateSnippet,
|
||||
deleteSnippet as deleteSnippetDB,
|
||||
seedDatabase,
|
||||
syncTemplatesFromJSON
|
||||
syncTemplatesFromJSON,
|
||||
getSnippetsByNamespace,
|
||||
ensureDefaultNamespace,
|
||||
getAllNamespaces
|
||||
} from '@/lib/db'
|
||||
|
||||
const templates = templatesData as SnippetTemplate[]
|
||||
@@ -37,26 +41,49 @@ export function SnippetManager() {
|
||||
const [viewerOpen, setViewerOpen] = useState(false)
|
||||
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null)
|
||||
const [viewingSnippet, setViewingSnippet] = useState<Snippet | null>(null)
|
||||
const [selectedNamespaceId, setSelectedNamespaceId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const loadSnippets = async () => {
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await seedDatabase()
|
||||
await syncTemplatesFromJSON(templates)
|
||||
const loadedSnippets = await getAllSnippets()
|
||||
setSnippets(loadedSnippets)
|
||||
|
||||
const namespaces = await getAllNamespaces()
|
||||
const defaultNamespace = namespaces.find(n => n.isDefault)
|
||||
if (defaultNamespace) {
|
||||
setSelectedNamespaceId(defaultNamespace.id)
|
||||
const loadedSnippets = await getSnippetsByNamespace(defaultNamespace.id)
|
||||
setSnippets(loadedSnippets)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load snippets:', error)
|
||||
toast.error('Failed to load snippets')
|
||||
console.error('Failed to load data:', error)
|
||||
toast.error('Failed to load data')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadSnippets()
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const loadSnippetsForNamespace = async () => {
|
||||
if (!selectedNamespaceId) return
|
||||
|
||||
try {
|
||||
const loadedSnippets = await getSnippetsByNamespace(selectedNamespaceId)
|
||||
setSnippets(loadedSnippets)
|
||||
} catch (error) {
|
||||
console.error('Failed to load snippets:', error)
|
||||
toast.error('Failed to load snippets')
|
||||
}
|
||||
}
|
||||
|
||||
loadSnippetsForNamespace()
|
||||
}, [selectedNamespaceId])
|
||||
|
||||
const filteredSnippets = useMemo(() => {
|
||||
const allSnippets = snippets || []
|
||||
if (!searchQuery.trim()) return allSnippets
|
||||
@@ -88,6 +115,7 @@ export function SnippetManager() {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
namespaceId: selectedNamespaceId || undefined,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
@@ -100,7 +128,7 @@ export function SnippetManager() {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [editingSnippet])
|
||||
}, [editingSnippet, selectedNamespaceId])
|
||||
|
||||
const handleEditSnippet = useCallback((snippet: Snippet) => {
|
||||
setEditingSnippet(snippet)
|
||||
@@ -171,6 +199,12 @@ export function SnippetManager() {
|
||||
if (allSnippets.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<NamespaceSelector
|
||||
selectedNamespaceId={selectedNamespaceId}
|
||||
onNamespaceChange={setSelectedNamespaceId}
|
||||
/>
|
||||
</div>
|
||||
<EmptyState
|
||||
onCreateClick={handleCreateNew}
|
||||
onCreateFromTemplate={handleCreateFromTemplate}
|
||||
@@ -187,6 +221,11 @@ export function SnippetManager() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<NamespaceSelector
|
||||
selectedNamespaceId={selectedNamespaceId}
|
||||
onNamespaceChange={setSelectedNamespaceId}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<div className="relative flex-1 w-full sm:max-w-md">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
|
||||
159
src/lib/db.ts
159
src/lib/db.ts
@@ -144,6 +144,15 @@ export async function initDB(): Promise<Database> {
|
||||
throw new Error('Failed to initialize database')
|
||||
}
|
||||
|
||||
dbInstance.run(`
|
||||
CREATE TABLE IF NOT EXISTS namespaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
createdAt INTEGER NOT NULL,
|
||||
isDefault INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
dbInstance.run(`
|
||||
CREATE TABLE IF NOT EXISTS snippets (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -152,11 +161,13 @@ export async function initDB(): Promise<Database> {
|
||||
code TEXT NOT NULL,
|
||||
language TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
namespaceId TEXT,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
|
||||
)
|
||||
`)
|
||||
|
||||
@@ -284,8 +295,8 @@ export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO snippets (id, title, description, code, language, category, hasPreview, functionName, inputParameters, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
snippet.id,
|
||||
snippet.title,
|
||||
@@ -293,6 +304,7 @@ export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.namespaceId || null,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
@@ -314,7 +326,7 @@ export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
|
||||
db.run(
|
||||
`UPDATE snippets
|
||||
SET title = ?, description = ?, code = ?, language = ?, category = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
|
||||
SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
snippet.title,
|
||||
@@ -322,6 +334,7 @@ export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.namespaceId || null,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
@@ -396,6 +409,8 @@ export async function createTemplate(template: SnippetTemplate): Promise<void> {
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
await ensureDefaultNamespace()
|
||||
|
||||
const checkSnippets = db.exec('SELECT COUNT(*) as count FROM snippets')
|
||||
const snippetCount = checkSnippets[0]?.values[0]?.[0] as number
|
||||
|
||||
@@ -858,3 +873,139 @@ export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllNamespaces(): Promise<import('./types').Namespace[]> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const namespace: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'isDefault') {
|
||||
namespace[col] = row[idx] === 1
|
||||
} else {
|
||||
namespace[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return namespace
|
||||
})
|
||||
}
|
||||
|
||||
export async function createNamespace(name: string): Promise<import('./types').Namespace> {
|
||||
const db = await initDB()
|
||||
|
||||
const namespace: import('./types').Namespace = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
isDefault: false
|
||||
}
|
||||
|
||||
db.run(
|
||||
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[namespace.id, namespace.name, namespace.createdAt, namespace.isDefault ? 1 : 0]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
return namespace
|
||||
}
|
||||
|
||||
export async function deleteNamespace(id: string): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
|
||||
if (defaultNamespace.length === 0 || defaultNamespace[0].values.length === 0) {
|
||||
throw new Error('Default namespace not found')
|
||||
}
|
||||
|
||||
const defaultId = defaultNamespace[0].values[0][0] as string
|
||||
|
||||
const checkDefault = db.exec('SELECT isDefault FROM namespaces WHERE id = ?', [id])
|
||||
if (checkDefault.length > 0 && checkDefault[0].values[0]?.[0] === 1) {
|
||||
throw new Error('Cannot delete default namespace')
|
||||
}
|
||||
|
||||
db.run('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', [defaultId, id])
|
||||
|
||||
db.run('DELETE FROM namespaces WHERE id = ?', [id])
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function ensureDefaultNamespace(): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT COUNT(*) as count FROM namespaces WHERE isDefault = 1')
|
||||
const count = results[0]?.values[0]?.[0] as number || 0
|
||||
|
||||
if (count === 0) {
|
||||
const defaultNamespace: import('./types').Namespace = {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
createdAt: Date.now(),
|
||||
isDefault: true
|
||||
}
|
||||
|
||||
db.run(
|
||||
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[defaultNamespace.id, defaultNamespace.name, defaultNamespace.createdAt, 1]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC', [namespaceId, namespaceId])
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const snippet: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
snippet[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
snippet[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return snippet as Snippet
|
||||
})
|
||||
}
|
||||
|
||||
export async function getNamespaceById(id: string): Promise<import('./types').Namespace | null> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id])
|
||||
|
||||
if (results.length === 0 || results[0].values.length === 0) return null
|
||||
|
||||
const columns = results[0].columns
|
||||
const row = results[0].values[0]
|
||||
|
||||
const namespace: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'isDefault') {
|
||||
namespace[col] = row[idx] === 1
|
||||
} else {
|
||||
namespace[col] = row[idx]
|
||||
}
|
||||
})
|
||||
|
||||
return namespace
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Snippet {
|
||||
code: string
|
||||
language: string
|
||||
category: string
|
||||
namespaceId?: string
|
||||
hasPreview?: boolean
|
||||
functionName?: string
|
||||
inputParameters?: InputParameter[]
|
||||
@@ -21,6 +22,13 @@ export interface Snippet {
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface Namespace {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: number
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
export interface SnippetTemplate {
|
||||
id: string
|
||||
title: string
|
||||
|
||||
Reference in New Issue
Block a user