Files
supabase/apps/studio/components/interfaces/Explorer/SaveQueryDropdown.tsx
T
Joshen Lim 09a1f431bc Mass update for all lucide icon imports to use the component with Icon suffix (#51659)
## Context

This is largely just a mechanical change here:
Opting to use lucide icons with the `Icon` suffix across all icon
imports within `studio`
- so that it’s clearer which components are icons + reduces the
possibility of name collision with our own components
- e.g <User /> vs <UserIcon />

Also added a eslint rule for this:
<img width="1069" height="167" alt="image"
src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc"
/>

And updated `AGENTS.md` as such:
```
Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced).
```
2026-10-14 12:56:55 +02:00

145 lines
4.7 KiB
TypeScript

import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { SaveIcon } 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 { createLogCellSkeleton, createQueryCellSkeleton } from './utils'
import { getNotebook } from '@/data/content/notebooks/notebook-query'
import { useNotebooksInfiniteQuery } from '@/data/content/notebooks/notebooks-infinite-query'
import { type QuerySourceBinding } from '@/data/query-sources/query-source-registry'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
interface SaveQueryDropdownProps {
query: { title: string; sql: string }
/** Saves as a log cell when the query targets logs. Defaults to a database cell. */
source?: QuerySourceBinding
}
export const SaveQueryDropdown = ({
children,
query,
source,
}: 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 createCell = () =>
source?._tag === 'logs'
? createLogCellSkeleton({ ...query, time_range: source.time_range })
: createQueryCellSkeleton(query)
const onAddToNewNotebook = () => {
createNotebook({
cells: [createCell()],
})
}
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: createCell(),
})
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={<SaveIcon 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>
)
}