| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- import { keepPreviousData } from '@tanstack/react-query'
- import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks'
- import { Plus } from 'lucide-react'
- import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
- import {
- Command,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuTrigger,
- ScrollArea,
- } from 'ui'
- import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
- import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
- import { useContentInfiniteQuery } from '@/data/content/content-infinite-query'
- import type { Content } from '@/data/content/content-query'
- import { SNIPPET_PAGE_LIMIT } from '@/data/content/sql-folders-query'
- import { editorPanelState } from '@/state/editor-panel-state'
- import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
- type SnippetDropdownProps = {
- projectRef?: string
- trigger: ReactNode
- side?: 'top' | 'bottom' | 'left' | 'right'
- align?: 'start' | 'center' | 'end'
- className?: string
- autoFocus?: boolean
- onSelect: (snippet: { id: string; name: string }) => void
- }
- type SqlContentItem = Extract<Content, { type: 'sql' }>
- export const SnippetDropdown = ({
- projectRef,
- trigger,
- side = 'bottom',
- align = 'end',
- className,
- autoFocus = false,
- onSelect,
- }: SnippetDropdownProps) => {
- const { openSidebar } = useSidebarManagerSnapshot()
- const scrollRootRef = useRef<HTMLDivElement | null>(null)
- const [open, setOpen] = useState(false)
- const [search, setSearch] = useState('')
- const debouncedSearch = useDebounce(search, 500)
- const {
- data,
- isPending: isLoading,
- hasNextPage,
- fetchNextPage,
- isFetchingNextPage,
- } = useContentInfiniteQuery(
- {
- projectRef,
- type: 'sql',
- limit: SNIPPET_PAGE_LIMIT,
- name: search.length === 0 ? search : debouncedSearch,
- },
- { placeholderData: keepPreviousData }
- )
- const snippets = useMemo(() => {
- const items = data?.pages.flatMap((page) => page.content) ?? []
- return items as SqlContentItem[]
- }, [data?.pages])
- const [sentinelRef, entry] = useIntersectionObserver({
- root: scrollRootRef.current,
- threshold: 0,
- rootMargin: '0px',
- })
- useEffect(() => {
- if (entry?.isIntersecting && hasNextPage && !isFetchingNextPage && !isLoading) {
- fetchNextPage()
- }
- }, [entry?.isIntersecting, hasNextPage, isFetchingNextPage, isLoading, fetchNextPage])
- return (
- <DropdownMenu open={open} onOpenChange={setOpen}>
- <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
- <DropdownMenuContent
- side={side}
- align={align}
- className={['w-80 p-0', className].filter(Boolean).join(' ')}
- >
- <Command shouldFilter={false}>
- <CommandInput
- showResetIcon
- autoFocus={autoFocus}
- placeholder="Search snippets..."
- value={search}
- onValueChange={setSearch}
- handleReset={() => setSearch('')}
- />
- <CommandList ref={scrollRootRef}>
- {isLoading ? (
- <p className="text-xs text-center text-foreground-lighter py-3">Loading...</p>
- ) : search.length > 0 && snippets.length === 0 ? (
- <p className="text-xs text-center text-foreground-lighter py-3">No snippets found</p>
- ) : (
- <CommandGroup>
- <ScrollArea className={snippets.length > 7 ? 'h-[210px]' : ''}>
- {snippets.map((snippet) => (
- <CommandItem
- key={snippet.id}
- value={snippet.id}
- onSelect={() => onSelect({ id: snippet.id, name: snippet.name })}
- >
- {snippet.name}
- </CommandItem>
- ))}
- <div ref={sentinelRef} className="h-1 -mt-1" />
- {hasNextPage && (
- <div className="px-2 py-1">
- <ShimmeringLoader className="py-2" />
- </div>
- )}
- </ScrollArea>
- </CommandGroup>
- )}
- <div className="h-px bg-border-overlay -mx-1" />
- <CommandGroup>
- <CommandItem
- className="cursor-pointer w-full"
- onSelect={() => {
- setOpen(false)
- editorPanelState.openAsNew()
- openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
- }}
- >
- <div className="w-full flex items-center gap-2">
- <Plus size={14} strokeWidth={1.5} />
- <p>Create snippet</p>
- </div>
- </CommandItem>
- </CommandGroup>
- </CommandList>
- </Command>
- </DropdownMenuContent>
- </DropdownMenu>
- )
- }
|