Files
supabase/apps/studio/components/interfaces/Explorer/SaveQueryDropdown.tsx
T
Joshen Lim ad5202d53d Joshenlim/fe 4378 update editorpanel save cta when explorer is enabled (#50324)
## Context

Swaps out the "Save as snippet" CTA in the `EditorPanel` to the "Save to
notebook" dropdown CTA if explorer is enabled
<img width="488" height="147" alt="image"
src="https://github.com/user-attachments/assets/915017a1-c416-4904-8146-b0695244d2bd"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a save-query dropdown in Explorer for creating a new notebook or
adding a query to an existing notebook.
* Added notebook search, loading states, and empty-state messaging when
selecting an existing notebook.
  * Saved queries now open in the selected notebook after insertion.

* **Improvements**
  * Added an accessible label to the query title input.
* Updated query-saving controls to appear appropriately when Explorer is
enabled.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-15 16:26:44 +08:00

133 lines
4.3 KiB
TypeScript

import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { Save } from 'lucide-react'
import { useRouter } from 'next/router'
import { useMemo, useState, type PropsWithChildren } from 'react'
import { toast } from 'sonner'
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { ExplorerToolbarAction } from './ExplorerToolbar'
import { useCreateNotebook } from './hooks'
import { createQueryCellSkeleton } from './utils'
import { getNotebook } from '@/data/content/notebooks/notebook-query'
import { useNotebooksInfiniteQuery } from '@/data/content/notebooks/notebooks-infinite-query'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
interface SaveQueryDropdownProps {
query: { title: string; sql: string }
}
export const SaveQueryDropdown = ({
children,
query,
}: PropsWithChildren<SaveQueryDropdownProps>) => {
const router = useRouter()
const { ref } = useParams()
const { createNotebook } = useCreateNotebook()
const notebooksSnap = useNotebooksStateSnapshot()
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 500)
const { data: notebooksData, isPending } = useNotebooksInfiniteQuery({
projectRef: ref,
limit: 100,
name: search.length === 0 ? search : debouncedSearch,
})
const notebooks = useMemo(() => {
const items = notebooksData?.pages.flatMap((page) => page.content) ?? []
return items
}, [notebooksData?.pages])
const onAddToNewNotebook = () => {
createNotebook({
cells: [createQueryCellSkeleton(query)],
})
}
const onAddToExistingNotebook = async (notebookId: string) => {
if (!ref) return
try {
if (!notebooksSnap.notebooks[notebookId]?.notebook.content) {
const notebook = await getNotebook({ projectRef: ref, id: notebookId })
notebooksSnap.setNotebook({ projectRef: ref, notebook })
}
notebooksSnap.insertCellAfter({
id: notebookId,
cell: createQueryCellSkeleton(query),
})
notebooksSnap.requestScrollToBottom(notebookId)
router.push(`/project/${ref}/explorer/notebook/${notebookId}`)
} catch (error) {
toast.error('Failed to add query to notebook')
}
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{children ?? (
<ExplorerToolbarAction icon={<Save size={16} strokeWidth={2} />} tooltip="Save query" />
)}
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" align="end">
<DropdownMenuSub>
<DropdownMenuSubTrigger>Add to existing notebook</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="p-0">
<Command shouldFilter={false}>
<CommandInput
autoFocus
placeholder="Search notebooks..."
className="text-xs"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandGroup>
{isPending ? (
<div className="flex flex-col p-1 gap-y-1">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
</div>
) : !notebooks?.length ? (
<p className="text-xs text-center text-foreground-lighter py-3">
No notebooks found
</p>
) : null}
{notebooks?.map((notebook) => (
<CommandItem
key={notebook.id}
value={notebook.id}
className="cursor-pointer"
onSelect={() => onAddToExistingNotebook(notebook.id)}
>
{notebook.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem onClick={onAddToNewNotebook}>Create a new notebook</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}