Skip to content

Commit 167f921

Browse files
committed
Add SignatureSelector component for custom signature input in webhooks (#7370)
# [INFRA-1454][Dashboard] Feature: Add SignatureSelector component for improved signature selection ## Notes for the reviewer This PR introduces a new `SignatureSelector` component that enhances the signature selection experience in the webhooks UI. The component allows users to either select from predefined signatures or enter custom ones, with appropriate UI feedback. Key improvements: - Users can now enter custom signatures directly in the input field - Warning message appears when a custom signature is entered - Automatic ABI detection for known signatures - Better handling of signature formats (automatically converts text signatures to hash format) ## How to test 1. Navigate to the webhooks creation flow 2. Test selecting predefined event/function signatures 3. Test entering custom signatures and verify the warning appears 4. Verify that ABIs are properly loaded for known signatures 5. Test the reset functionality The component is fully integrated with the existing form validation system. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new signature selector with search and custom input support for selecting or entering function signatures. - Enhanced dropdown components to allow custom search input elements. - **Refactor** - Replaced the previous signature dropdown with the new selector for improved usability and consistency. - Improved ABI input handling, including automatic detection and reset options. - **Bug Fixes** - Improved normalization of signature hash values and ABI fallback logic for webhook payloads. - Enhanced address parsing for ABI reset functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces enhancements to the `webhookPayloadUtils`, `MultiSelect`, and `SignatureSelector` components, improving signature handling and custom input functionality. It also updates the `FilterDetailsStep` to utilize the new `SignatureSelector` and remove the deprecated `SignatureDropdown`. ### Detailed summary - Updated `buildEventWebhookPayload` and `buildTransactionWebhookPayload` functions to handle `sigHash` and `abi` more robustly. - Added `customSearchInput` prop to `MultiSelect` for enhanced search functionality. - Implemented `SignatureSelector` component for better signature selection with custom input support. - Replaced `SignatureDropdown` with `SignatureSelector` in `FilterDetailsStep`. - Improved handling of custom signatures and ABI management in `FilterDetailsStep`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent bb09e20 commit 167f921

File tree

4 files changed

+320
-210
lines changed

4 files changed

+320
-210
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { cn } from "@/lib/utils";
2+
import { useCallback, useMemo, useRef, useState } from "react";
3+
import { MultiSelect } from "./multi-select";
4+
5+
interface SignatureOption {
6+
label: string;
7+
value: string;
8+
abi?: string;
9+
}
10+
11+
interface SignatureSelectorProps {
12+
options: SignatureOption[];
13+
value: string;
14+
onChange: (val: string) => void;
15+
setAbi?: (abi: string) => void;
16+
placeholder?: string;
17+
disabled?: boolean;
18+
secondaryTextFormatter?: (sig: SignatureOption) => string;
19+
className?: string;
20+
}
21+
22+
export function SignatureSelector({
23+
options,
24+
value,
25+
onChange,
26+
setAbi,
27+
placeholder = "Select or enter a signature",
28+
disabled,
29+
secondaryTextFormatter,
30+
className,
31+
}: SignatureSelectorProps) {
32+
const [searchValue, setSearchValue] = useState("");
33+
const inputRef = useRef<HTMLInputElement>(null);
34+
35+
// Memoize options with formatted secondary text if provided
36+
const formattedOptions = useMemo(() => {
37+
return options.map((opt) => ({
38+
...opt,
39+
label: secondaryTextFormatter
40+
? `${opt.label}${secondaryTextFormatter(opt)}`
41+
: opt.label,
42+
}));
43+
}, [options, secondaryTextFormatter]);
44+
45+
// Check if the current value is a custom value (not in options)
46+
const isCustomValue = value && !options.some((opt) => opt.value === value);
47+
48+
// Add the custom value as an option if needed
49+
const allOptions = useMemo(() => {
50+
if (isCustomValue && value) {
51+
return [...formattedOptions, { label: value, value }];
52+
}
53+
return formattedOptions;
54+
}, [formattedOptions, isCustomValue, value]);
55+
56+
// Single-select MultiSelect wrapper
57+
const handleSelectedValuesChange = useCallback(
58+
(selected: string[]) => {
59+
// Always use the last selected value for single-select behavior
60+
const selectedValue =
61+
selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
62+
onChange(selectedValue);
63+
const found = options.find((opt) => opt.value === selectedValue);
64+
if (setAbi) {
65+
setAbi(found?.abi || "");
66+
}
67+
setSearchValue("");
68+
},
69+
[onChange, setAbi, options],
70+
);
71+
72+
// Handle custom value entry
73+
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
74+
if (event.key === "Enter" && searchValue.trim()) {
75+
if (!options.some((opt) => opt.value === searchValue.trim())) {
76+
onChange(searchValue.trim());
77+
if (setAbi) setAbi("");
78+
setSearchValue("");
79+
// Optionally blur input
80+
inputRef.current?.blur();
81+
}
82+
}
83+
};
84+
85+
// Custom render for MultiSelect's search input
86+
const customSearchInput = (
87+
<input
88+
ref={inputRef}
89+
type="text"
90+
className={cn(
91+
"w-full border-0 border-border border-b bg-transparent py-4 pr-2 pl-10 text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
92+
disabled && "cursor-not-allowed opacity-50",
93+
)}
94+
placeholder={placeholder}
95+
value={searchValue}
96+
onChange={(e) => setSearchValue(e.target.value)}
97+
onKeyDown={handleInputKeyDown}
98+
disabled={disabled}
99+
autoComplete="off"
100+
/>
101+
);
102+
103+
return (
104+
<div className={className}>
105+
<MultiSelect
106+
options={allOptions}
107+
selectedValues={value ? [value] : []}
108+
onSelectedValuesChange={handleSelectedValuesChange}
109+
placeholder={placeholder}
110+
maxCount={1}
111+
disabled={disabled}
112+
searchPlaceholder={placeholder}
113+
customTrigger={null}
114+
renderOption={(option) => <span>{option.label}</span>}
115+
overrideSearchFn={(option, searchTerm) =>
116+
option.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
117+
option.value.toLowerCase().includes(searchTerm.toLowerCase())
118+
}
119+
customSearchInput={customSearchInput}
120+
/>
121+
{isCustomValue && (
122+
<div className="mt-2 rounded border border-warning-200 bg-warning-50 px-2 py-1 text-warning-700 text-xs">
123+
You entered a custom signature. Please provide the ABI below.
124+
</div>
125+
)}
126+
</div>
127+
);
128+
}

apps/dashboard/src/@/components/blocks/multi-select.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ interface MultiSelectProps
5353
align?: "center" | "start" | "end";
5454
side?: "left" | "right" | "top" | "bottom";
5555
showSelectedValuesInModal?: boolean;
56+
customSearchInput?: React.ReactNode;
5657
}
5758

5859
export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
@@ -69,6 +70,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
6970
searchPlaceholder,
7071
popoverContentClassName,
7172
showSelectedValuesInModal = false,
73+
customSearchInput,
7274
...props
7375
},
7476
ref,
@@ -233,15 +235,19 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
233235
>
234236
{/* Search */}
235237
<div className="relative">
236-
<Input
237-
placeholder={searchPlaceholder || "Search"}
238-
value={searchValue}
239-
// do not focus on the input when the popover opens to avoid opening the keyboard
240-
tabIndex={-1}
241-
onChange={(e) => setSearchValue(e.target.value)}
242-
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
243-
onKeyDown={handleInputKeyDown}
244-
/>
238+
{customSearchInput ? (
239+
customSearchInput
240+
) : (
241+
<Input
242+
placeholder={searchPlaceholder || "Search"}
243+
value={searchValue}
244+
// do not focus on the input when the popover opens to avoid opening the keyboard
245+
tabIndex={-1}
246+
onChange={(e) => setSearchValue(e.target.value)}
247+
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
248+
onKeyDown={handleInputKeyDown}
249+
/>
250+
)}
245251
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
246252
</div>
247253

0 commit comments

Comments
 (0)