Skip to content

feat: enhance aggregate parameter input with multi-select presets #7168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
"use client";

import { MultiSelect } from "@/components/blocks/multi-select";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ControllerRenderProps } from "react-hook-form";

interface Preset {
Expand Down Expand Up @@ -145,53 +140,137 @@ interface AggregateParameterInputProps {
},
string
>;
showTip: boolean;
hasError: boolean;
placeholder: string;
endpointPath: string; // New prop
showTip?: boolean;
hasError?: boolean;
placeholder?: string;
endpointPath: string;
}

export function AggregateParameterInput(props: AggregateParameterInputProps) {
const { field, showTip, hasError, placeholder, endpointPath } = props;
const { field, placeholder, endpointPath, showTip } = props;
const { value, onChange } = field;

const presets = getAggregatePresets(endpointPath);
const presets = useMemo(
() => getAggregatePresets(endpointPath),
[endpointPath],
);

const selectedValues = useMemo(() => {
if (!value) return [];
return Array.from(
new Set(
String(value)
.split(",")
.map((v) => v.trim()) // remove leading / trailing spaces
.filter(Boolean),
),
);
}, [value]);

const handlePresetChange = useCallback(
(values: string[]) => {
onChange({ target: { value: values.join(",") } });
},
[onChange],
);

// Custom search function for the MultiSelect
const searchFunction = useCallback(
(option: { value: string; label: string }, searchTerm: string) => {
if (!searchTerm) return true;
const query = searchTerm.toLowerCase();
return (
option.label.toLowerCase().includes(query) ||
option.value.toLowerCase().includes(query)
);
},
[],
);

// Get display values for the selected items
useCallback(
(value: string) => {
const preset = presets.find((p) => p.value === value);
return preset ? preset.label : value;
},
[presets],
);

// Format selected values for display in the MultiSelect
useMemo(() => {
return selectedValues.map((value) => {
const preset = presets.find((p) => p.value === value);
return {
label: preset?.label || value,
value,
};
});
}, [selectedValues, presets]);

// State for the manual input text
const [manualInput, setManualInput] = useState("");

// Update manual input when selected values change
useEffect(() => {
if (selectedValues.length === 0) {
setManualInput("");
} else {
setManualInput(selectedValues.join(", "));
}
}, [selectedValues]);

// Handle manual input changes
const handleManualInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setManualInput(value);

// Update selected values by splitting on commas and trimming whitespace
const newValues = value
.split(",")
.map((v) => v.trim())
.filter(Boolean);

onChange({ target: { value: newValues.join(",") } });
};

return (
<div className="flex flex-col space-y-1">
<Input
{...field}
<div className="w-full">
{/* Editable formula text field */}
<div className="relative">
<Input
value={manualInput}
onChange={handleManualInputChange}
placeholder={placeholder}
className={cn(
"h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
showTip && "lg:pr-10",
)}
/>
</div>

{/* MultiSelect for choosing aggregations */}
<MultiSelect
options={presets}
selectedValues={selectedValues}
onSelectedValuesChange={handlePresetChange}
placeholder="Select presets (optional)"
searchPlaceholder="Search aggregation presets"
className={cn(
"h-auto truncate rounded-none border-0 bg-transparent py-5 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
showTip && "lg:pr-10",
hasError && "text-destructive-text",
"rounded-none border-0 border-border border-t-2 border-dashed",
"hover:bg-inherit",
)}
popoverContentClassName="min-w-[calc(100vw-20px)] lg:min-w-[500px]"
selectedBadgeClassName="font-normal"
overrideSearchFn={searchFunction}
renderOption={(option) => (
<div className="flex w-full items-center justify-between">
<span className="truncate">{option.label}</span>
<span className="ml-2 truncate font-mono text-muted-foreground text-xs">
{option.value}
</span>
</div>
)}
placeholder={placeholder}
/>
<Select
value={presets.find((p) => p.value === value)?.value || ""}
onValueChange={(selectedValue) => {
if (selectedValue) {
onChange({ target: { value: selectedValue } });
}
}}
>
<SelectTrigger
className={cn(
"h-8 border-dashed bg-transparent text-xs focus:ring-0 focus:ring-offset-0",
!presets.find((p) => p.value === value) && "text-muted-foreground",
)}
>
<SelectValue placeholder="Select a preset (optional)" />
</SelectTrigger>
<SelectContent className="font-mono">
{presets.map((preset) => (
<SelectItem key={preset.value} value={preset.value}>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,9 @@ function ParameterSection(props: {
<div className="flex flex-col gap-2">
{description && (
<p className="text-foreground">
{description}
{param.name === "aggregate"
? "Aggregation(s). You can type in multiple, separated by a comma, or select from the presets"
: description}
</p>
)}

Expand All @@ -536,7 +538,9 @@ function ParameterSection(props: {
<p className="mb-1 text-muted-foreground">
Example:{" "}
<span className="font-mono">
{exampleToShow}
{param.name === "aggregate"
? "count() AS count_all, countDistinct(address) AS unique_addresses"
: exampleToShow}
</span>
</p>
</div>
Expand All @@ -547,7 +551,10 @@ function ParameterSection(props: {
<Button
asChild
variant="ghost"
className="-translate-y-1/2 absolute top-1/2 right-2 hidden h-auto w-auto p-1.5 text-muted-foreground opacity-50 hover:opacity-100 lg:flex"
className={cn(
"-translate-y-1/2 absolute top-1/2 right-2 hidden h-auto w-auto p-1.5 text-muted-foreground opacity-50 hover:opacity-100 lg:flex",
param.name === "aggregate" && "top-[21px]",
)}
>
<div>
<InfoIcon className="size-4" />
Expand Down
Loading