|
1 | 1 | "use client";
|
2 | 2 |
|
| 3 | +import { MultiSelect } from "@/components/blocks/multi-select"; |
3 | 4 | import { Input } from "@/components/ui/input";
|
4 |
| -import { |
5 |
| - Select, |
6 |
| - SelectContent, |
7 |
| - SelectItem, |
8 |
| - SelectTrigger, |
9 |
| - SelectValue, |
10 |
| -} from "@/components/ui/select"; |
11 | 5 | import { cn } from "@/lib/utils";
|
| 6 | +import { useCallback, useEffect, useMemo, useState } from "react"; |
12 | 7 | import type { ControllerRenderProps } from "react-hook-form";
|
13 | 8 |
|
14 | 9 | interface Preset {
|
@@ -145,53 +140,137 @@ interface AggregateParameterInputProps {
|
145 | 140 | },
|
146 | 141 | string
|
147 | 142 | >;
|
148 |
| - showTip: boolean; |
149 |
| - hasError: boolean; |
150 |
| - placeholder: string; |
151 |
| - endpointPath: string; // New prop |
| 143 | + showTip?: boolean; |
| 144 | + hasError?: boolean; |
| 145 | + placeholder?: string; |
| 146 | + endpointPath: string; |
152 | 147 | }
|
153 | 148 |
|
154 | 149 | export function AggregateParameterInput(props: AggregateParameterInputProps) {
|
155 |
| - const { field, showTip, hasError, placeholder, endpointPath } = props; |
| 150 | + const { field, placeholder, endpointPath, showTip } = props; |
156 | 151 | const { value, onChange } = field;
|
157 | 152 |
|
158 |
| - const presets = getAggregatePresets(endpointPath); |
| 153 | + const presets = useMemo( |
| 154 | + () => getAggregatePresets(endpointPath), |
| 155 | + [endpointPath], |
| 156 | + ); |
| 157 | + |
| 158 | + const selectedValues = useMemo(() => { |
| 159 | + if (!value) return []; |
| 160 | + return Array.from( |
| 161 | + new Set( |
| 162 | + String(value) |
| 163 | + .split(",") |
| 164 | + .map((v) => v.trim()) // remove leading / trailing spaces |
| 165 | + .filter(Boolean), |
| 166 | + ), |
| 167 | + ); |
| 168 | + }, [value]); |
| 169 | + |
| 170 | + const handlePresetChange = useCallback( |
| 171 | + (values: string[]) => { |
| 172 | + onChange({ target: { value: values.join(",") } }); |
| 173 | + }, |
| 174 | + [onChange], |
| 175 | + ); |
| 176 | + |
| 177 | + // Custom search function for the MultiSelect |
| 178 | + const searchFunction = useCallback( |
| 179 | + (option: { value: string; label: string }, searchTerm: string) => { |
| 180 | + if (!searchTerm) return true; |
| 181 | + const query = searchTerm.toLowerCase(); |
| 182 | + return ( |
| 183 | + option.label.toLowerCase().includes(query) || |
| 184 | + option.value.toLowerCase().includes(query) |
| 185 | + ); |
| 186 | + }, |
| 187 | + [], |
| 188 | + ); |
| 189 | + |
| 190 | + // Get display values for the selected items |
| 191 | + useCallback( |
| 192 | + (value: string) => { |
| 193 | + const preset = presets.find((p) => p.value === value); |
| 194 | + return preset ? preset.label : value; |
| 195 | + }, |
| 196 | + [presets], |
| 197 | + ); |
| 198 | + |
| 199 | + // Format selected values for display in the MultiSelect |
| 200 | + useMemo(() => { |
| 201 | + return selectedValues.map((value) => { |
| 202 | + const preset = presets.find((p) => p.value === value); |
| 203 | + return { |
| 204 | + label: preset?.label || value, |
| 205 | + value, |
| 206 | + }; |
| 207 | + }); |
| 208 | + }, [selectedValues, presets]); |
| 209 | + |
| 210 | + // State for the manual input text |
| 211 | + const [manualInput, setManualInput] = useState(""); |
| 212 | + |
| 213 | + // Update manual input when selected values change |
| 214 | + useEffect(() => { |
| 215 | + if (selectedValues.length === 0) { |
| 216 | + setManualInput(""); |
| 217 | + } else { |
| 218 | + setManualInput(selectedValues.join(", ")); |
| 219 | + } |
| 220 | + }, [selectedValues]); |
| 221 | + |
| 222 | + // Handle manual input changes |
| 223 | + const handleManualInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| 224 | + const value = e.target.value; |
| 225 | + setManualInput(value); |
| 226 | + |
| 227 | + // Update selected values by splitting on commas and trimming whitespace |
| 228 | + const newValues = value |
| 229 | + .split(",") |
| 230 | + .map((v) => v.trim()) |
| 231 | + .filter(Boolean); |
| 232 | + |
| 233 | + onChange({ target: { value: newValues.join(",") } }); |
| 234 | + }; |
159 | 235 |
|
160 | 236 | return (
|
161 |
| - <div className="flex flex-col space-y-1"> |
162 |
| - <Input |
163 |
| - {...field} |
| 237 | + <div className="w-full"> |
| 238 | + {/* Editable formula text field */} |
| 239 | + <div className="relative"> |
| 240 | + <Input |
| 241 | + value={manualInput} |
| 242 | + onChange={handleManualInputChange} |
| 243 | + placeholder={placeholder} |
| 244 | + className={cn( |
| 245 | + "h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0", |
| 246 | + showTip && "lg:pr-10", |
| 247 | + )} |
| 248 | + /> |
| 249 | + </div> |
| 250 | + |
| 251 | + {/* MultiSelect for choosing aggregations */} |
| 252 | + <MultiSelect |
| 253 | + options={presets} |
| 254 | + selectedValues={selectedValues} |
| 255 | + onSelectedValuesChange={handlePresetChange} |
| 256 | + placeholder="Select presets (optional)" |
| 257 | + searchPlaceholder="Search aggregation presets" |
164 | 258 | 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", |
| 259 | + "rounded-none border-0 border-border border-t-2 border-dashed", |
| 260 | + "hover:bg-inherit", |
| 261 | + )} |
| 262 | + popoverContentClassName="min-w-[calc(100vw-20px)] lg:min-w-[500px]" |
| 263 | + selectedBadgeClassName="font-normal" |
| 264 | + overrideSearchFn={searchFunction} |
| 265 | + renderOption={(option) => ( |
| 266 | + <div className="flex w-full items-center justify-between"> |
| 267 | + <span className="truncate">{option.label}</span> |
| 268 | + <span className="ml-2 truncate font-mono text-muted-foreground text-xs"> |
| 269 | + {option.value} |
| 270 | + </span> |
| 271 | + </div> |
168 | 272 | )}
|
169 |
| - placeholder={placeholder} |
170 | 273 | />
|
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> |
195 | 274 | </div>
|
196 | 275 | );
|
197 | 276 | }
|
0 commit comments