Skip to content
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
110 changes: 86 additions & 24 deletions apps/dashboard/src/@/components/blocks/SignatureSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ interface SignatureOption {

interface SignatureSelectorProps {
options: SignatureOption[];
value: string;
onChange: (val: string) => void;
value: string | string[];
onChange: (val: string | string[]) => void;
setAbi?: (abi: string) => void;
placeholder?: string;
disabled?: boolean;
secondaryTextFormatter?: (sig: SignatureOption) => string;
className?: string;
multiSelect?: boolean;
}

export function SignatureSelector({
Expand All @@ -28,6 +29,7 @@ export function SignatureSelector({
disabled,
secondaryTextFormatter,
className,
multiSelect = false,
}: SignatureSelectorProps) {
const [searchValue, setSearchValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
Expand All @@ -42,38 +44,96 @@ export function SignatureSelector({
}));
}, [options, secondaryTextFormatter]);

// Check if the current value is a custom value (not in options)
const isCustomValue = value && !options.some((opt) => opt.value === value);
// Handle both single and multi-select values
const currentValues = useMemo((): string[] => {
if (multiSelect) {
if (Array.isArray(value)) {
return value.filter(
(val): val is string =>
val !== undefined && val !== null && val !== "",
);
} else {
return value ? [value] : [];
}
} else {
if (Array.isArray(value)) {
return value.length > 0 && value[0] ? [value[0]] : [];
} else {
return value ? [value] : [];
}
}
}, [value, multiSelect]);

// Check if the current values include custom values (not in options)
const customValues = useMemo((): string[] => {
return currentValues.filter(
(val): val is string =>
val !== undefined &&
val !== null &&
val !== "" &&
!options.some((opt) => opt.value === val),
);
}, [currentValues, options]);

// Add the custom value as an option if needed
// Add the custom values as options if needed
const allOptions = useMemo(() => {
if (isCustomValue && value) {
return [...formattedOptions, { label: value, value }];
}
return formattedOptions;
}, [formattedOptions, isCustomValue, value]);
const customOptions = customValues.map((val) => ({
label: val,
value: val,
}));
return [...formattedOptions, ...customOptions];
}, [formattedOptions, customValues]);

// Single-select MultiSelect wrapper
// Multi-select or single-select MultiSelect wrapper
const handleSelectedValuesChange = useCallback(
(selected: string[]) => {
// Always use the last selected value for single-select behavior
const selectedValue =
selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
onChange(selectedValue);
const found = options.find((opt) => opt.value === selectedValue);
if (setAbi) {
setAbi(found?.abi || "");
if (multiSelect) {
// Multi-select behavior
onChange(selected);
// For multi-select, we'll use the ABI from the first selected option that has one
const firstOptionWithAbi = selected.find((selectedValue) => {
const found = options.find((opt) => opt.value === selectedValue);
return found?.abi;
});
if (setAbi && firstOptionWithAbi) {
const found = options.find((opt) => opt.value === firstOptionWithAbi);
setAbi(found?.abi || "");
}
} else {
// Single-select behavior (maintain backward compatibility)
const selectedValue =
selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
onChange(selectedValue);
const found = options.find((opt) => opt.value === selectedValue);
if (setAbi) {
setAbi(found?.abi || "");
}
}
setSearchValue("");
},
[onChange, setAbi, options],
[onChange, setAbi, options, multiSelect],
);

// Handle custom value entry
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && searchValue.trim()) {
if (!options.some((opt) => opt.value === searchValue.trim())) {
onChange(searchValue.trim());
if (multiSelect) {
// Add to existing values for multi-select
const currentArray = Array.isArray(value)
? value
: value
? [value]
: [];
const filteredArray = currentArray.filter(
(val): val is string => val !== undefined && val !== null,
);
const newValues = [...filteredArray, searchValue.trim()];
onChange(newValues);
} else {
Comment on lines +123 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Also de-duplicate when adding custom values

The manual-input path appends searchValue without checking for duplicates, leading to repeated entries. Apply the same new Set(...) pattern here.

🤖 Prompt for AI Agents
In apps/dashboard/src/@/components/blocks/SignatureSelector.tsx around lines 123
to 133, the code appends searchValue to the array without removing duplicates,
causing repeated entries. To fix this, wrap the combined array of filteredArray
and searchValue.trim() with a new Set to eliminate duplicates before converting
it back to an array. Then pass this deduplicated array to onChange.

// Replace value for single-select
onChange(searchValue.trim());
}
if (setAbi) setAbi("");
setSearchValue("");
// Optionally blur input
Expand Down Expand Up @@ -106,7 +166,7 @@ export function SignatureSelector({
customSearchInput={customSearchInput}
customTrigger={null}
disabled={disabled}
maxCount={1}
maxCount={multiSelect ? 100 : 1}
onSelectedValuesChange={handleSelectedValuesChange}
options={allOptions}
overrideSearchFn={(option, searchTerm) =>
Expand All @@ -116,11 +176,13 @@ export function SignatureSelector({
placeholder={placeholder}
renderOption={(option) => <span>{option.label}</span>}
searchPlaceholder={placeholder}
selectedValues={value ? [value] : []}
selectedValues={currentValues}
/>
{isCustomValue && (
{customValues.length > 0 && (
<div className="mt-2 rounded border border-warning-200 bg-warning-50 px-2 py-1 text-warning-700 text-xs">
You entered a custom signature. Please provide the ABI below.
{multiSelect
? `You entered ${customValues.length} custom signature${customValues.length > 1 ? "s" : ""}. Please provide the ABI below.`
: "You entered a custom signature. Please provide the ABI below."}
</div>
)}
</div>
Expand Down
10 changes: 8 additions & 2 deletions apps/dashboard/src/@/components/blocks/multi-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
popoverContentClassName,
showSelectedValuesInModal = false,
customSearchInput,
customTrigger,
...props
},
ref,
Expand Down Expand Up @@ -144,13 +145,18 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
// scroll to top when options change
const popoverElRef = useRef<HTMLDivElement>(null);

// Filter out customTrigger from props to avoid passing it to Button
const buttonProps = Object.fromEntries(
Object.entries(props).filter(([key]) => key !== "customTrigger"),
) as React.ButtonHTMLAttributes<HTMLButtonElement>;

return (
<Popover modal onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
<PopoverTrigger asChild>
{props.customTrigger || (
{customTrigger || (
<Button
ref={ref}
{...props}
{...buttonProps}
className={cn(
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
className,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function CreateContractWebhookButton({
inputAbi: [],
name: "",
secret: "",
sigHash: "",
sigHash: [],
sigHashAbi: "",
toAddresses: "",
webhookUrl: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,16 @@ export function FilterDetailsStep({
const knownFunctionSignatures = functionSignatures.map(
(sig) => sig.signature,
);

// Handle both single and multiple signatures for custom signature detection
const sigHashes = Array.isArray(sigHash) ? sigHash : sigHash ? [sigHash] : [];
const isCustomSignature =
(watchFilterType === "event" &&
sigHash &&
!knownEventSignatures.includes(sigHash)) ||
sigHashes.some((hash) => hash && !knownEventSignatures.includes(hash))) ||
(watchFilterType === "transaction" &&
sigHash &&
!knownFunctionSignatures.includes(sigHash));
sigHashes.some(
(hash) => hash && !knownFunctionSignatures.includes(hash),
));

return (
<>
Expand Down Expand Up @@ -140,7 +143,7 @@ export function FilterDetailsStep({
</div>
<FormControl>
<div className="space-y-2">
<Input placeholder="0x1234..." {...field} />
<Input placeholder="0x1234...,0xabcd..." {...field} />

{/* ABI fetch status */}
<div className="mt-2 flex items-center justify-between">
Expand Down Expand Up @@ -300,13 +303,13 @@ export function FilterDetailsStep({
<div className="flex items-center justify-between text-xs">
<FormLabel>
{watchFilterType === "event"
? "Event Signature (optional)"
: "Function Signature (optional)"}
? "Event Signatures (optional)"
: "Function Signatures (optional)"}
</FormLabel>
<p className="text-muted-foreground">
{watchFilterType === "event"
? "Select an event to monitor"
: "Select a function to monitor"}
? "Select events to monitor"
: "Select functions to monitor"}
</p>
</div>
<FormControl>
Expand All @@ -315,11 +318,16 @@ export function FilterDetailsStep({
eventSignatures.length > 0 ? (
<SignatureSelector
className="block w-full max-w-90 overflow-hidden text-ellipsis"
multiSelect={true}
onChange={(val) => {
field.onChange(val);
// If custom signature, clear ABI field
const known = eventSignatures.map((sig) => sig.signature);
if (val && !known.includes(val)) {
const values = Array.isArray(val) ? val : [val];
const hasCustomSignature = values.some(
(v) => v && !known.includes(v),
);
if (hasCustomSignature) {
form.setValue("abi", "");
}
}}
Expand All @@ -328,21 +336,26 @@ export function FilterDetailsStep({
label: truncateMiddle(sig.name, 30, 15),
value: sig.signature,
}))}
placeholder="Select or enter an event signature"
placeholder="Select or enter event signatures"
setAbi={(abi) => form.setValue("sigHashAbi", abi)}
value={field.value || ""}
value={field.value || []}
/>
) : watchFilterType === "transaction" &&
Object.keys(fetchedTxAbis).length > 0 &&
functionSignatures.length > 0 ? (
<SignatureSelector
multiSelect={true}
onChange={(val) => {
field.onChange(val);
// If custom signature, clear ABI field
const known = functionSignatures.map(
(sig) => sig.signature,
);
if (val && !known.includes(val)) {
const values = Array.isArray(val) ? val : [val];
const hasCustomSignature = values.some(
(v) => v && !known.includes(v),
);
if (hasCustomSignature) {
form.setValue("abi", "");
}
}}
Expand All @@ -351,9 +364,9 @@ export function FilterDetailsStep({
label: truncateMiddle(sig.name, 30, 15),
value: sig.signature,
}))}
placeholder="Select or enter a function signature"
placeholder="Select or enter function signatures"
setAbi={(abi) => form.setValue("sigHashAbi", abi)}
value={field.value || ""}
value={field.value || []}
/>
) : (
<Input
Expand All @@ -367,7 +380,11 @@ export function FilterDetailsStep({
? "Fetching event signatures..."
: "Fetching function signatures..."
}
value={field.value}
value={
Array.isArray(field.value)
? field.value.join(", ")
: field.value || ""
}
/>
)}
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,27 @@ export default function ReviewStep({

<li className="flex justify-between">
<span className="text-muted-foreground text-sm">
Signature Hash:
Signature Hash
{Array.isArray(form.watch("sigHash")) &&
(form.watch("sigHash")?.length || 0) > 1
? "es"
: ""}
:
</span>
<span className="font-medium text-sm">
{(() => {
const sigHash = form.watch("sigHash");
return sigHash ? truncateMiddle(sigHash, 10, 6) : "None";
if (!sigHash) return "None";

if (Array.isArray(sigHash)) {
if (sigHash.length === 0) return "None";
if (sigHash.length === 1) {
return truncateMiddle(sigHash[0] || "", 10, 6);
}
return `${sigHash.length} signature${sigHash.length > 1 ? "s" : ""} selected`;
} else {
return truncateMiddle(sigHash, 10, 6);
}
})()}
</span>
</li>
Expand Down
Loading
Loading