PublicationsComboBox.tsx153 lines · main
1import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react'
2import { useEffect, useState } from 'react'
3import { ControllerRenderProps } from 'react-hook-form'
4import {
5 Badge,
6 Button,
7 cn,
8 Command,
9 CommandEmpty,
10 CommandGroup,
11 CommandInput,
12 CommandItem,
13 CommandList,
14 CommandSeparator,
15 Popover,
16 PopoverContent,
17 PopoverTrigger,
18 ScrollArea,
19} from 'ui'
20
21import type { ReplicationPublication } from '@/data/replication/publications-query'
22
23interface PublicationsComboBoxProps {
24 publications: ReplicationPublication[]
25 isLoadingPublications: boolean
26 onNewPublicationClick: () => void
27 field: ControllerRenderProps<any, 'publicationName'>
28}
29
30export const PublicationsComboBox = ({
31 publications,
32 isLoadingPublications,
33 onNewPublicationClick,
34 field,
35}: PublicationsComboBoxProps) => {
36 const [dropdownOpen, setDropdownOpen] = useState(false)
37 const [selectedPublication, setSelectedPublication] = useState<string>(field?.value || '')
38 const [searchTerm, setSearchTerm] = useState('')
39
40 function handlePublicationSelect(pub: string) {
41 setSelectedPublication(pub)
42 setDropdownOpen(false)
43 field.onChange(pub)
44 }
45
46 useEffect(() => {
47 setSelectedPublication(field?.value || '')
48 }, [field?.value])
49
50 return (
51 <Popover
52 modal={false}
53 open={dropdownOpen}
54 onOpenChange={(open) => {
55 setDropdownOpen(open)
56 if (!open && field?.onBlur) {
57 field.onBlur()
58 }
59 }}
60 >
61 <PopoverTrigger asChild>
62 <Button
63 type="default"
64 size="medium"
65 className={cn(
66 'w-full [&>span]:w-full text-left',
67 !selectedPublication && 'text-foreground-muted'
68 )}
69 iconRight={<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />}
70 name={field.name}
71 onBlur={field.onBlur}
72 >
73 {selectedPublication || 'Select publication'}
74 </Button>
75 </PopoverTrigger>
76 <PopoverContent sameWidthAsTrigger className="p-0" align="start">
77 <Command>
78 <CommandInput
79 placeholder="Find publication..."
80 className="text-xs"
81 value={searchTerm}
82 onValueChange={setSearchTerm}
83 />
84 <div className="px-2 pt-2 pb-1">
85 <p className="text-xs text-foreground-lighter">
86 Publications with no tables are hidden
87 </p>
88 </div>
89 <CommandList>
90 <CommandEmpty>
91 {isLoadingPublications ? (
92 <div className="flex items-center gap-2 text-center justify-center">
93 <Loader2 size={12} className="animate-spin" />
94 Loading...
95 </div>
96 ) : (
97 'No publications found'
98 )}
99 </CommandEmpty>
100
101 <CommandGroup>
102 {publications.length === 0 && (
103 <p className="text-foreground-lighter text-xs py-3 px-2">
104 No publications available
105 </p>
106 )}
107 <ScrollArea className={publications.length > 7 ? 'h-[210px]' : ''}>
108 {publications.map((pub) => (
109 <CommandItem
110 key={pub.name}
111 className="cursor-pointer flex items-center justify-between space-x-2 w-full"
112 onSelect={() => {
113 handlePublicationSelect(pub.name)
114 }}
115 onClick={() => {
116 handlePublicationSelect(pub.name)
117 }}
118 >
119 <span>{pub.name}</span>
120 <div className="flex items-center gap-2">
121 <Badge
122 variant="default"
123 className="rounded-full px-2 py-0.5 text-[10px] font-normal border border-border bg-surface-100"
124 >
125 {pub.tables.length} {pub.tables.length === 1 ? 'table' : 'tables'}
126 </Badge>
127 {selectedPublication === pub.name && (
128 <Check className="text-brand" strokeWidth={2} size={13} />
129 )}
130 </div>
131 </CommandItem>
132 ))}
133 </ScrollArea>
134 </CommandGroup>
135
136 <CommandSeparator />
137
138 <CommandGroup>
139 <CommandItem
140 className="cursor-pointer w-full"
141 onSelect={onNewPublicationClick}
142 onClick={onNewPublicationClick}
143 >
144 <Plus size={14} strokeWidth={1.5} className="mr-2" />
145 <p>New publication</p>
146 </CommandItem>
147 </CommandGroup>
148 </CommandList>
149 </Command>
150 </PopoverContent>
151 </Popover>
152 )
153}