Skip to content

Commit ce6615d

Browse files
committed
webhook multiselect topic
1 parent f7ce6be commit ce6615d

File tree

8 files changed

+177
-96
lines changed

8 files changed

+177
-96
lines changed

apps/dashboard/src/@/components/blocks/SignatureSelector.tsx

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ interface SignatureOption {
1010

1111
interface SignatureSelectorProps {
1212
options: SignatureOption[];
13-
value: string;
14-
onChange: (val: string) => void;
13+
value: string | string[];
14+
onChange: (val: string | string[]) => void;
1515
setAbi?: (abi: string) => void;
1616
placeholder?: string;
1717
disabled?: boolean;
1818
secondaryTextFormatter?: (sig: SignatureOption) => string;
1919
className?: string;
20+
multiSelect?: boolean;
2021
}
2122

2223
export function SignatureSelector({
@@ -28,6 +29,7 @@ export function SignatureSelector({
2829
disabled,
2930
secondaryTextFormatter,
3031
className,
32+
multiSelect = false,
3133
}: SignatureSelectorProps) {
3234
const [searchValue, setSearchValue] = useState("");
3335
const inputRef = useRef<HTMLInputElement>(null);
@@ -42,38 +44,79 @@ export function SignatureSelector({
4244
}));
4345
}, [options, secondaryTextFormatter]);
4446

45-
// Check if the current value is a custom value (not in options)
46-
const isCustomValue = value && !options.some((opt) => opt.value === value);
47+
// Handle both single and multi-select values
48+
const currentValues = useMemo((): string[] => {
49+
if (multiSelect) {
50+
if (Array.isArray(value)) {
51+
return value.filter((val): val is string => val !== undefined && val !== null);
52+
} else {
53+
return value ? [value] : [];
54+
}
55+
} else {
56+
if (Array.isArray(value)) {
57+
return value.length > 0 && value[0] ? [value[0]] : [];
58+
} else {
59+
return value ? [value] : [];
60+
}
61+
}
62+
}, [value, multiSelect]);
63+
64+
// Check if the current values include custom values (not in options)
65+
const customValues = useMemo(() => {
66+
return currentValues.filter((val): val is string => val !== undefined && val !== null && !options.some((opt) => opt.value === val));
67+
}, [currentValues, options]);
4768

48-
// Add the custom value as an option if needed
69+
// Add the custom values as options if needed
4970
const allOptions = useMemo(() => {
50-
if (isCustomValue && value) {
51-
return [...formattedOptions, { label: value, value }];
52-
}
53-
return formattedOptions;
54-
}, [formattedOptions, isCustomValue, value]);
71+
const customOptions = customValues
72+
.filter(val => val) // Filter out undefined/null values
73+
.map(val => ({ label: val!, value: val! }));
74+
return [...formattedOptions, ...customOptions];
75+
}, [formattedOptions, customValues]);
5576

56-
// Single-select MultiSelect wrapper
77+
// Multi-select or single-select MultiSelect wrapper
5778
const handleSelectedValuesChange = useCallback(
5879
(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 || "");
80+
if (multiSelect) {
81+
// Multi-select behavior
82+
onChange(selected);
83+
// For multi-select, we'll use the ABI from the first selected option that has one
84+
const firstOptionWithAbi = selected.find(selectedValue => {
85+
const found = options.find((opt) => opt.value === selectedValue);
86+
return found?.abi;
87+
});
88+
if (setAbi && firstOptionWithAbi) {
89+
const found = options.find((opt) => opt.value === firstOptionWithAbi);
90+
setAbi(found?.abi || "");
91+
}
92+
} else {
93+
// Single-select behavior (maintain backward compatibility)
94+
const selectedValue = selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
95+
onChange(selectedValue);
96+
const found = options.find((opt) => opt.value === selectedValue);
97+
if (setAbi) {
98+
setAbi(found?.abi || "");
99+
}
66100
}
67101
setSearchValue("");
68102
},
69-
[onChange, setAbi, options],
103+
[onChange, setAbi, options, multiSelect],
70104
);
71105

72106
// Handle custom value entry
73107
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
74108
if (event.key === "Enter" && searchValue.trim()) {
75109
if (!options.some((opt) => opt.value === searchValue.trim())) {
76-
onChange(searchValue.trim());
110+
if (multiSelect) {
111+
// Add to existing values for multi-select
112+
const currentArray = Array.isArray(value) ? value : (value ? [value] : []);
113+
const filteredArray = currentArray.filter((val): val is string => val !== undefined && val !== null);
114+
const newValues = [...filteredArray, searchValue.trim()];
115+
onChange(newValues);
116+
} else {
117+
// Replace value for single-select
118+
onChange(searchValue.trim());
119+
}
77120
if (setAbi) setAbi("");
78121
setSearchValue("");
79122
// Optionally blur input
@@ -106,7 +149,7 @@ export function SignatureSelector({
106149
customSearchInput={customSearchInput}
107150
customTrigger={null}
108151
disabled={disabled}
109-
maxCount={1}
152+
maxCount={multiSelect ? 100 : 1}
110153
onSelectedValuesChange={handleSelectedValuesChange}
111154
options={allOptions}
112155
overrideSearchFn={(option, searchTerm) =>
@@ -116,11 +159,14 @@ export function SignatureSelector({
116159
placeholder={placeholder}
117160
renderOption={(option) => <span>{option.label}</span>}
118161
searchPlaceholder={placeholder}
119-
selectedValues={value ? [value] : []}
162+
selectedValues={currentValues}
120163
/>
121-
{isCustomValue && (
164+
{customValues.length > 0 && (
122165
<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.
166+
{multiSelect
167+
? `You entered ${customValues.length} custom signature${customValues.length > 1 ? 's' : ''}. Please provide the ABI below.`
168+
: "You entered a custom signature. Please provide the ABI below."
169+
}
124170
</div>
125171
)}
126172
</div>

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
6464
popoverContentClassName,
6565
showSelectedValuesInModal = false,
6666
customSearchInput,
67+
customTrigger,
6768
...props
6869
},
6970
ref,
@@ -144,13 +145,18 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
144145
// scroll to top when options change
145146
const popoverElRef = useRef<HTMLDivElement>(null);
146147

148+
// Filter out customTrigger from props to avoid passing it to Button
149+
const buttonProps = Object.fromEntries(
150+
Object.entries(props).filter(([key]) => key !== 'customTrigger')
151+
) as React.ButtonHTMLAttributes<HTMLButtonElement>;
152+
147153
return (
148154
<Popover modal onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
149155
<PopoverTrigger asChild>
150-
{props.customTrigger || (
156+
{customTrigger || (
151157
<Button
152158
ref={ref}
153-
{...props}
159+
{...buttonProps}
154160
className={cn(
155161
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
156162
className,

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/CreateWebhookModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function CreateContractWebhookButton({
6565
inputAbi: [],
6666
name: "",
6767
secret: "",
68-
sigHash: "",
68+
sigHash: [],
6969
sigHashAbi: "",
7070
toAddresses: "",
7171
webhookUrl: "",

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/FilterDetailsStep.tsx

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,14 @@ export function FilterDetailsStep({
7474
const knownFunctionSignatures = functionSignatures.map(
7575
(sig) => sig.signature,
7676
);
77+
78+
// Handle both single and multiple signatures for custom signature detection
79+
const sigHashes = Array.isArray(sigHash) ? sigHash : (sigHash ? [sigHash] : []);
7780
const isCustomSignature =
7881
(watchFilterType === "event" &&
79-
sigHash &&
80-
!knownEventSignatures.includes(sigHash)) ||
82+
sigHashes.some(hash => hash && !knownEventSignatures.includes(hash))) ||
8183
(watchFilterType === "transaction" &&
82-
sigHash &&
83-
!knownFunctionSignatures.includes(sigHash));
84+
sigHashes.some(hash => hash && !knownFunctionSignatures.includes(hash)));
8485

8586
return (
8687
<>
@@ -140,7 +141,7 @@ export function FilterDetailsStep({
140141
</div>
141142
<FormControl>
142143
<div className="space-y-2">
143-
<Input placeholder="0x1234..." {...field} />
144+
<Input placeholder="0x1234...,0xabcd..." {...field} />
144145

145146
{/* ABI fetch status */}
146147
<div className="mt-2 flex items-center justify-between">
@@ -300,13 +301,13 @@ export function FilterDetailsStep({
300301
<div className="flex items-center justify-between text-xs">
301302
<FormLabel>
302303
{watchFilterType === "event"
303-
? "Event Signature (optional)"
304-
: "Function Signature (optional)"}
304+
? "Event Signatures (optional)"
305+
: "Function Signatures (optional)"}
305306
</FormLabel>
306307
<p className="text-muted-foreground">
307308
{watchFilterType === "event"
308-
? "Select an event to monitor"
309-
: "Select a function to monitor"}
309+
? "Select events to monitor"
310+
: "Select functions to monitor"}
310311
</p>
311312
</div>
312313
<FormControl>
@@ -315,11 +316,14 @@ export function FilterDetailsStep({
315316
eventSignatures.length > 0 ? (
316317
<SignatureSelector
317318
className="block w-full max-w-90 overflow-hidden text-ellipsis"
319+
multiSelect={true}
318320
onChange={(val) => {
319321
field.onChange(val);
320322
// If custom signature, clear ABI field
321323
const known = eventSignatures.map((sig) => sig.signature);
322-
if (val && !known.includes(val)) {
324+
const values = Array.isArray(val) ? val : [val];
325+
const hasCustomSignature = values.some(v => v && !known.includes(v));
326+
if (hasCustomSignature) {
323327
form.setValue("abi", "");
324328
}
325329
}}
@@ -328,21 +332,24 @@ export function FilterDetailsStep({
328332
label: truncateMiddle(sig.name, 30, 15),
329333
value: sig.signature,
330334
}))}
331-
placeholder="Select or enter an event signature"
335+
placeholder="Select or enter event signatures"
332336
setAbi={(abi) => form.setValue("sigHashAbi", abi)}
333-
value={field.value || ""}
337+
value={field.value || []}
334338
/>
335339
) : watchFilterType === "transaction" &&
336340
Object.keys(fetchedTxAbis).length > 0 &&
337341
functionSignatures.length > 0 ? (
338342
<SignatureSelector
343+
multiSelect={true}
339344
onChange={(val) => {
340345
field.onChange(val);
341346
// If custom signature, clear ABI field
342347
const known = functionSignatures.map(
343348
(sig) => sig.signature,
344349
);
345-
if (val && !known.includes(val)) {
350+
const values = Array.isArray(val) ? val : [val];
351+
const hasCustomSignature = values.some(v => v && !known.includes(v));
352+
if (hasCustomSignature) {
346353
form.setValue("abi", "");
347354
}
348355
}}
@@ -351,9 +358,9 @@ export function FilterDetailsStep({
351358
label: truncateMiddle(sig.name, 30, 15),
352359
value: sig.signature,
353360
}))}
354-
placeholder="Select or enter a function signature"
361+
placeholder="Select or enter function signatures"
355362
setAbi={(abi) => form.setValue("sigHashAbi", abi)}
356-
value={field.value || ""}
363+
value={field.value || []}
357364
/>
358365
) : (
359366
<Input
@@ -367,7 +374,7 @@ export function FilterDetailsStep({
367374
? "Fetching event signatures..."
368375
: "Fetching function signatures..."
369376
}
370-
value={field.value}
377+
value={Array.isArray(field.value) ? field.value.join(", ") : field.value || ""}
371378
/>
372379
)}
373380
</FormControl>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/ReviewStep.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,22 @@ export default function ReviewStep({
194194

195195
<li className="flex justify-between">
196196
<span className="text-muted-foreground text-sm">
197-
Signature Hash:
197+
Signature Hash{Array.isArray(form.watch("sigHash")) && form.watch("sigHash")?.length > 1 ? "es" : ""}:
198198
</span>
199199
<span className="font-medium text-sm">
200200
{(() => {
201201
const sigHash = form.watch("sigHash");
202-
return sigHash ? truncateMiddle(sigHash, 10, 6) : "None";
202+
if (!sigHash) return "None";
203+
204+
if (Array.isArray(sigHash)) {
205+
if (sigHash.length === 0) return "None";
206+
if (sigHash.length === 1) {
207+
return truncateMiddle(sigHash[0], 10, 6);
208+
}
209+
return `${sigHash.length} signature${sigHash.length > 1 ? 's' : ''} selected`;
210+
} else {
211+
return truncateMiddle(sigHash, 10, 6);
212+
}
203213
})()}
204214
</span>
205215
</li>

0 commit comments

Comments
 (0)