|
1 | 1 | "use client";
|
2 | 2 |
|
3 |
| -import { Input } from "@/components/ui/input"; |
4 |
| -import { |
5 |
| - Select, |
6 |
| - SelectContent, |
7 |
| - SelectItem, |
8 |
| - SelectTrigger, |
9 |
| - SelectValue, |
10 |
| -} from "@/components/ui/select"; |
11 | 3 | import { cn } from "@/lib/utils";
|
12 | 4 | import type { ControllerRenderProps } from "react-hook-form";
|
| 5 | +import { MultiSelect } from "@/components/blocks/multi-select"; |
| 6 | +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 7 | +import { Input } from "@/components/ui/input"; |
| 8 | +import { Badge } from "@/components/ui/badge"; |
| 9 | +import { Button } from "@/components/ui/button"; |
| 10 | +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; |
| 11 | +import { ChevronDownIcon, SearchIcon, XIcon } from "lucide-react"; |
13 | 12 |
|
14 | 13 | interface Preset {
|
15 | 14 | label: string;
|
@@ -145,53 +144,109 @@ interface AggregateParameterInputProps {
|
145 | 144 | },
|
146 | 145 | string
|
147 | 146 | >;
|
148 |
| - showTip: boolean; |
149 |
| - hasError: boolean; |
150 |
| - placeholder: string; |
151 |
| - endpointPath: string; // New prop |
| 147 | + showTip?: boolean; |
| 148 | + hasError?: boolean; |
| 149 | + placeholder?: string; |
| 150 | + endpointPath: string; |
152 | 151 | }
|
153 | 152 |
|
154 | 153 | export function AggregateParameterInput(props: AggregateParameterInputProps) {
|
155 |
| - const { field, showTip, hasError, placeholder, endpointPath } = props; |
| 154 | + const { field, placeholder, endpointPath } = props; |
156 | 155 | const { value, onChange } = field;
|
| 156 | + const [searchQuery, setSearchQuery] = useState(''); |
| 157 | + const inputRef = useRef<HTMLInputElement>(null); |
| 158 | + const [isPopoverOpen, setIsPopoverOpen] = useState(false); |
| 159 | + |
| 160 | + const presets = useMemo(() => getAggregatePresets(endpointPath), [endpointPath]); |
| 161 | + |
| 162 | + const selectedValues = useMemo(() => { |
| 163 | + if (!value) return []; |
| 164 | + return String(value).split(',').filter(Boolean); |
| 165 | + }, [value]); |
| 166 | + |
| 167 | + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| 168 | + onChange(e); |
| 169 | + }; |
157 | 170 |
|
158 |
| - const presets = getAggregatePresets(endpointPath); |
| 171 | + const handlePresetSelect = useCallback((preset: { value: string; label: string }) => { |
| 172 | + const newValue = value ? `${value}, ${preset.value}` : preset.value; |
| 173 | + onChange({ target: { value: newValue } }); |
| 174 | + inputRef.current?.focus(); |
| 175 | + }, [value, onChange]); |
159 | 176 |
|
160 | 177 | return (
|
161 |
| - <div className="flex flex-col space-y-1"> |
| 178 | + <div className="w-full space-y-2"> |
| 179 | + {/* Main input field */} |
162 | 180 | <Input
|
163 |
| - {...field} |
164 |
| - className={cn( |
165 |
| - "h-auto truncate rounded-none border-0 bg-transparent py-5 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0", |
166 |
| - showTip && "lg:pr-10", |
167 |
| - hasError && "text-destructive-text", |
168 |
| - )} |
169 |
| - placeholder={placeholder} |
| 181 | + ref={inputRef} |
| 182 | + value={value || ''} |
| 183 | + onChange={handleInputChange} |
| 184 | + placeholder={placeholder || "Enter aggregation formula..."} |
| 185 | + className="w-full font-mono text-sm" |
170 | 186 | />
|
171 |
| - <Select |
172 |
| - value={presets.find((p) => p.value === value)?.value || ""} |
173 |
| - onValueChange={(selectedValue) => { |
174 |
| - if (selectedValue) { |
175 |
| - onChange({ target: { value: selectedValue } }); |
176 |
| - } |
177 |
| - }} |
178 |
| - > |
179 |
| - <SelectTrigger |
180 |
| - className={cn( |
181 |
| - "h-8 border-dashed bg-transparent text-xs focus:ring-0 focus:ring-offset-0", |
182 |
| - !presets.find((p) => p.value === value) && "text-muted-foreground", |
183 |
| - )} |
184 |
| - > |
185 |
| - <SelectValue placeholder="Select a preset (optional)" /> |
186 |
| - </SelectTrigger> |
187 |
| - <SelectContent className="font-mono"> |
188 |
| - {presets.map((preset) => ( |
189 |
| - <SelectItem key={preset.value} value={preset.value}> |
190 |
| - {preset.label} |
191 |
| - </SelectItem> |
192 |
| - ))} |
193 |
| - </SelectContent> |
194 |
| - </Select> |
| 187 | + |
| 188 | + {/* Preset selector */} |
| 189 | + <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> |
| 190 | + <PopoverTrigger asChild> |
| 191 | + <Button |
| 192 | + variant="outline" |
| 193 | + size="sm" |
| 194 | + className="w-full justify-between text-muted-foreground" |
| 195 | + type="button" |
| 196 | + > |
| 197 | + <span>Select from presets</span> |
| 198 | + <ChevronDownIcon className="h-4 w-4" /> |
| 199 | + </Button> |
| 200 | + </PopoverTrigger> |
| 201 | + <PopoverContent className="w-[500px] p-0" align="start"> |
| 202 | + <div className="p-2 border-b"> |
| 203 | + <div className="relative"> |
| 204 | + <SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> |
| 205 | + <Input |
| 206 | + value={searchQuery} |
| 207 | + onChange={(e) => setSearchQuery(e.target.value)} |
| 208 | + placeholder="Search aggregations..." |
| 209 | + className="pl-8 h-9" |
| 210 | + /> |
| 211 | + </div> |
| 212 | + </div> |
| 213 | + <div className="max-h-[300px] overflow-auto p-1"> |
| 214 | + {presets |
| 215 | + .filter(preset => |
| 216 | + !searchQuery || |
| 217 | + preset.label.toLowerCase().includes(searchQuery.toLowerCase()) || |
| 218 | + preset.value.toLowerCase().includes(searchQuery.toLowerCase()) |
| 219 | + ) |
| 220 | + .map((preset) => ( |
| 221 | + <button |
| 222 | + key={preset.value} |
| 223 | + className="w-full text-left p-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md flex items-center justify-between" |
| 224 | + onClick={() => handlePresetSelect(preset)} |
| 225 | + type="button" |
| 226 | + > |
| 227 | + <span>{preset.label}</span> |
| 228 | + <span className="text-xs text-muted-foreground font-mono ml-2"> |
| 229 | + {preset.value} |
| 230 | + </span> |
| 231 | + </button> |
| 232 | + ))} |
| 233 | + </div> |
| 234 | + </PopoverContent> |
| 235 | + </Popover> |
| 236 | + |
| 237 | + {/* Selected presets as badges */} |
| 238 | + {selectedValues.length > 0 && ( |
| 239 | + <div className="flex flex-wrap gap-1"> |
| 240 | + {selectedValues.map((val) => { |
| 241 | + const preset = presets.find(p => p.value === val); |
| 242 | + return ( |
| 243 | + <Badge key={val} variant="secondary" className="font-normal"> |
| 244 | + {preset?.label || val} |
| 245 | + </Badge> |
| 246 | + ); |
| 247 | + })} |
| 248 | + </div> |
| 249 | + )} |
195 | 250 | </div>
|
196 | 251 | );
|
197 | 252 | }
|
0 commit comments