Skip to content

Commit 74cd019

Browse files
committed
Add SelectWithSearch + Improved Single Network Selector component (#4957)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/4586e4df-ba4e-4d02-b594-61c61c81f446.png) <!-- start pr-codex --> --- ## PR-Codex overview This PR refactors the network selection components by replacing `NetworkDropdown` with `SingleNetworkSelector` across multiple files, enhancing the network selection UI and functionality. ### Detailed summary - Removed `NetworkDropdown` from various components. - Introduced `SingleNetworkSelector` to replace `NetworkDropdown`. - Updated props for `SingleNetworkSelector` to improve chain selection. - Refactored related imports to point to the new component location. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent f5d2d35 commit 74cd019

File tree

9 files changed

+353
-110
lines changed

9 files changed

+353
-110
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,15 @@
1+
"use client";
2+
13
import { MultiSelect } from "@/components/blocks/multi-select";
4+
import { SelectWithSearch } from "@/components/blocks/select-with-search";
25
import { Badge } from "@/components/ui/badge";
3-
import { Select } from "chakra-react-select";
4-
import type { SizeProp } from "chakra-react-select";
56
import { useCallback, useMemo } from "react";
6-
import { useFormContext } from "react-hook-form";
77
import { useAllChainsData } from "../../../hooks/chains/allChains";
88

9-
interface NetworkDropdownProps {
10-
useCleanChainName?: boolean;
11-
isDisabled?: boolean;
12-
onSingleChange: (networksEnabled: number) => void;
13-
value: number | undefined;
14-
size?: SizeProp;
15-
}
16-
179
function cleanChainName(chainName: string) {
1810
return chainName.replace("Mainnet", "");
1911
}
2012

21-
export const NetworkDropdown: React.FC<NetworkDropdownProps> = ({
22-
useCleanChainName = true,
23-
onSingleChange,
24-
value,
25-
size = "md",
26-
}) => {
27-
const form = useFormContext();
28-
const { allChains } = useAllChainsData();
29-
30-
const options = useMemo(() => {
31-
return allChains.map((chain) => {
32-
return {
33-
label: useCleanChainName
34-
? cleanChainName(chain.name)
35-
: `${chain.name} (${chain.chainId})`,
36-
value: chain.chainId,
37-
};
38-
});
39-
}, [allChains, useCleanChainName]);
40-
41-
const defaultValues = useMemo(() => {
42-
const networksEnabled = form?.watch(
43-
"networksForDeployment.networksEnabled",
44-
);
45-
46-
if (networksEnabled) {
47-
return options.filter(({ value: val }) =>
48-
form.watch("networksForDeployment.networksEnabled")?.includes(val),
49-
);
50-
}
51-
return options;
52-
}, [form, options]);
53-
54-
return (
55-
<div className="flex w-full flex-row items-center gap-2">
56-
<Select
57-
size={size}
58-
placeholder={"Select a network"}
59-
selectedOptionStyle="check"
60-
hideSelectedOptions={false}
61-
options={options}
62-
defaultValue={defaultValues}
63-
onChange={(selectedChain) => {
64-
if (selectedChain) {
65-
if (onSingleChange) {
66-
onSingleChange(selectedChain.value);
67-
}
68-
}
69-
}}
70-
chakraStyles={{
71-
container: (provided) => ({
72-
...provided,
73-
width: "full",
74-
}),
75-
downChevron: (provided) => ({
76-
...provided,
77-
color: "hsl(var(--text-muted-foreground)/50%)",
78-
}),
79-
dropdownIndicator: (provided) => ({
80-
...provided,
81-
color: "hsl(var(--text-muted-foreground)/50%)",
82-
}),
83-
control: (provided) => ({
84-
...provided,
85-
borderRadius: "lg",
86-
minWidth: "178px",
87-
}),
88-
}}
89-
value={options.find(({ value: val }) => val === value)}
90-
/>
91-
</div>
92-
);
93-
};
94-
9513
type Option = { label: string; value: string };
9614

9715
export function MultiNetworkSelector(props: {
@@ -160,3 +78,68 @@ export function MultiNetworkSelector(props: {
16078
/>
16179
);
16280
}
81+
82+
export function SingleNetworkSelector(props: {
83+
chainId: number | undefined;
84+
onChange: (chainId: number) => void;
85+
}) {
86+
const { allChains, idToChain } = useAllChainsData();
87+
88+
const options = useMemo(() => {
89+
return allChains.map((chain) => {
90+
return {
91+
label: chain.name,
92+
value: String(chain.chainId),
93+
};
94+
});
95+
}, [allChains]);
96+
97+
const searchFn = useCallback(
98+
(option: Option, searchValue: string) => {
99+
const chain = idToChain.get(Number(option.value));
100+
if (!chain) {
101+
return false;
102+
}
103+
104+
if (Number.isInteger(Number.parseInt(searchValue))) {
105+
return String(chain.chainId).startsWith(searchValue);
106+
}
107+
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
108+
},
109+
[idToChain],
110+
);
111+
112+
const renderOption = useCallback(
113+
(option: Option) => {
114+
const chain = idToChain.get(Number(option.value));
115+
if (!chain) {
116+
return option.label;
117+
}
118+
119+
return (
120+
<div className="flex justify-between gap-4">
121+
<span className="grow truncate text-left">{chain.name}</span>
122+
<Badge variant="outline" className="gap-2">
123+
<span className="text-muted-foreground">Chain ID</span>
124+
{chain.chainId}
125+
</Badge>
126+
</div>
127+
);
128+
},
129+
[idToChain],
130+
);
131+
132+
return (
133+
<SelectWithSearch
134+
searchPlaceholder="Search by Name or Chain Id"
135+
value={String(props.chainId)}
136+
options={options}
137+
onValueChange={(chainId) => {
138+
props.onChange(Number(chainId));
139+
}}
140+
placeholder="Select Chain"
141+
overrideSearchFn={searchFn}
142+
renderOption={renderOption}
143+
/>
144+
);
145+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { useMemo, useState } from "react";
3+
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
4+
import { SelectWithSearch } from "./select-with-search";
5+
6+
const meta = {
7+
title: "blocks/SelectWithSearch",
8+
component: Story,
9+
parameters: {
10+
nextjs: {
11+
appDirectory: true,
12+
},
13+
},
14+
} satisfies Meta<typeof Story>;
15+
16+
export default meta;
17+
type Story = StoryObj<typeof meta>;
18+
19+
export const Desktop: Story = {
20+
args: {},
21+
};
22+
23+
export const Mobile: Story = {
24+
args: {},
25+
parameters: {
26+
viewport: mobileViewport("iphone14"),
27+
},
28+
};
29+
30+
function createList(len: number) {
31+
return Array.from({ length: len }, (_, i) => ({
32+
value: `${i}`,
33+
label: `Item ${i}`,
34+
}));
35+
}
36+
37+
function Story() {
38+
return (
39+
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
40+
<VariantTest storyLabel="5 items" listLen={5} />
41+
<VariantTest storyLabel="5000 items" listLen={5000} />
42+
<VariantTest
43+
defaultValue={"3"}
44+
storyLabel="20 items, 3 selected by default"
45+
listLen={20}
46+
/>
47+
</div>
48+
);
49+
}
50+
51+
function VariantTest(props: {
52+
defaultValue?: string;
53+
storyLabel: string;
54+
listLen: number;
55+
}) {
56+
const list = useMemo(() => createList(props.listLen), [props.listLen]);
57+
const [value, setValue] = useState<string | undefined>(props.defaultValue);
58+
59+
return (
60+
<BadgeContainer label={props.storyLabel}>
61+
<SelectWithSearch
62+
value={value}
63+
options={list}
64+
onValueChange={setValue}
65+
placeholder="Select items"
66+
/>
67+
</BadgeContainer>
68+
);
69+
}

0 commit comments

Comments
 (0)