feat(notebooks): add Import .dpnb UI in sidebar

- "+" button now opens a menu with "New notebook" and "Import .dpnb…"
- File picker accepts .dpnb files, parses and recreates the notebook
  with all cells and pinned results intact
- Fix type narrowing in tab-query-editor (early return for notebook tabs)
This commit is contained in:
Rohith Gilla 2026-04-14 19:30:30 +05:30
parent 3069e60419
commit cbdbef2043
No known key found for this signature in database
2 changed files with 85 additions and 12 deletions

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { BookOpen, ChevronRight, Plus, MoreHorizontal, Trash2, FolderOpen } from 'lucide-react'
import { useState, useEffect, useRef } from 'react'
import { BookOpen, ChevronRight, Plus, MoreHorizontal, Trash2, FolderOpen, Upload } from 'lucide-react'
import {
Collapsible,
@ -23,6 +23,7 @@ import {
import { useNotebookStore } from '@/stores/notebook-store'
import { useConnectionStore, useTabStore, notify } from '@/stores'
import { parseDpnb } from './notebook-export'
import type { Notebook } from '@shared/index'
export function NotebookSidebar() {
@ -38,6 +39,7 @@ export function NotebookSidebar() {
const [isExpanded, setIsExpanded] = useState(false)
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
const fileInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (!isInitialized) {
@ -65,6 +67,59 @@ export function NotebookSidebar() {
notify.success('Notebook deleted', `"${nb.title}" was removed.`)
}
const handleImportClick = () => {
if (!activeConnectionId) {
notify.error('No connection', 'Connect to a database before importing a notebook.')
return
}
fileInputRef.current?.click()
}
const handleFileSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
e.target.value = ''
if (!file || !activeConnectionId) return
try {
const text = await file.text()
const parsed = parseDpnb(text)
if (!parsed) {
notify.error('Invalid file', 'The file is not a valid .dpnb notebook.')
return
}
const nb = await createNotebook({
title: parsed.title,
connectionId: activeConnectionId,
folder: parsed.folder
})
if (!nb) {
notify.error('Import failed', 'Could not create the notebook.')
return
}
for (let i = 0; i < parsed.cells.length; i++) {
const cell = parsed.cells[i]
const addResult = await window.api.notebooks.addCell(nb.id, {
type: cell.type,
content: cell.content,
order: i
})
if (!addResult.success || !addResult.data) continue
if (cell.pinnedResult) {
await window.api.notebooks.updateCell(addResult.data.id, {
pinnedResult: cell.pinnedResult
})
}
}
createNotebookTab(nb.connectionId, nb.id, nb.title)
notify.success('Notebook imported', `"${parsed.title}" is ready.`)
} catch (err) {
notify.error('Import failed', err instanceof Error ? err.message : 'Unknown error')
}
}
const toggleFolder = (folder: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev)
@ -130,15 +185,33 @@ export function NotebookSidebar() {
/>
<span>Notebooks</span>
</CollapsibleTrigger>
<SidebarGroupAction
onClick={(e) => {
e.stopPropagation()
handleCreate()
}}
title="New notebook"
>
<Plus className="size-3.5" />
</SidebarGroupAction>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarGroupAction
onClick={(e) => e.stopPropagation()}
title="New or import notebook"
>
<Plus className="size-3.5" />
</SidebarGroupAction>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem onClick={handleCreate}>
<Plus className="text-muted-foreground" />
<span>New notebook</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleImportClick}>
<Upload className="text-muted-foreground" />
<span>Import .dpnb</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<input
ref={fileInputRef}
type="file"
accept=".dpnb,application/json"
onChange={handleFileSelected}
style={{ display: 'none' }}
/>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>

View file

@ -1092,7 +1092,7 @@ export function TabQueryEditor({ tabId }: TabQueryEditorProps) {
}
}, [handleRunQuery, tab, tabConnection])
if (!tab) {
if (!tab || tab.type === 'notebook') {
return null
}