Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit7a0510a

Browse files
committed
refactor: show icons for multi-select parameter options
1 parentfdf458e commit7a0510a

File tree

4 files changed

+325
-124
lines changed

4 files changed

+325
-124
lines changed

‎docs/about/contributing/frontend.md

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -259,18 +259,16 @@ We use [Formik](https://formik.org/docs) for forms along with
259259

260260
##Testing
261261

262-
We use three types of testing in our app:**End-to-end (E2E)**,**Integration**
262+
We use three types of testing in our app:**End-to-end (E2E)**,**Integration/Unit**
263263
and**Visual Testing**.
264264

265-
###End-to-End (E2E)
265+
###End-to-End (E2E) – Playwright
266266

267267
These are useful for testing complete flows like "Create a user", "Import
268-
template", etc. We use[Playwright](https://playwright.dev/). If you only need
269-
to test if the page is being rendered correctly, you should consider using the
270-
**Visual Testing** approach.
268+
template", etc. We use[Playwright](https://playwright.dev/). These tests run against a full Coder instance, backed by a database, and allows you to make sure that features work properly all the way through the stack. "End to end", so to speak.
271269

272-
For scenarios where you need to be authenticated, you can use
273-
`test.use({ storageState: getStatePath("authState") })`.
270+
For scenarios where you need to be authenticated as a certain user, you can use
271+
`login` helper. Passing it some user credentials will log out of any other user account, and will attempt to login using those credentials.
274272

275273
For ease of debugging, it's possible to run a Playwright test in headful mode
276274
running a Playwright server on your local machine, and executing the test inside
@@ -289,22 +287,14 @@ local machine and forward the necessary ports to your workspace. At the end of
289287
the script, you will land_inside_ your workspace with environment variables set
290288
so you can simply execute the test (`pnpm run playwright:test`).
291289

292-
###Integration
290+
###Integration/Unit – Jest
293291

294-
Test user interactions like "Click in a button shows a dialog", "Submit the form
295-
sends the correct data", etc. For this, we use[Jest](https://jestjs.io/) and
296-
[react-testing-library](https://testing-library.com/docs/react-testing-library/intro/).
297-
If the test involves routing checks like redirects or maybe checking the info on
298-
another page, you should probably consider using the**E2E** approach.
292+
We use Jest mostly for testing code that does_not_ pertain to React. Functions and classes that contain notable app logic, and which are well abstracted from React should have accompanying tests. If the logic is tightly coupled to a React component, a Storybook test or an E2E test may be a better option depending on the scenario.
299293

300-
###Visualtesting
294+
###VisualTesting – Storybook
301295

302-
We use visual tests to test components without user interaction like testing if
303-
a page/component is rendered correctly depending on some parameters, if a button
304-
is showing a spinner, if`loading` props are passed correctly, etc. This should
305-
always be your first option since it is way easier to maintain. For this, we use
306-
[Storybook](https://storybook.js.org/) and
307-
[Chromatic](https://www.chromatic.com/).
296+
We use Storybook for testing all of our React code. For static components, you simply add a story that renders the components with the props that you would like to test, and Storybook will record snapshots of it to ensure that it isn't changed unintentionally. If you would like to test an interaction with the component, then you can add an interaction test by specifying a`play` function for the story. For stories with an interaction test, a snapshot will be recorded of the end state of the component. We use
297+
[Chromatic](https://www.chromatic.com/) to manage and compare snapshots in CI.
308298

309299
To learn more about testing components that fetch API data, refer to the
310300
[**Where to fetch data**](#where-to-fetch-data) section.

‎site/src/components/MultiSelectCombobox/MultiSelectCombobox.stories.tsx

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,184 @@ export const ClearAllComboboxItems: Story = {
9898
);
9999
},
100100
};
101+
102+
exportconstWithIcons:Story={
103+
args:{
104+
placeholder:"Select technology",
105+
emptyIndicator:(
106+
<pclassName="text-center text-md text-content-primary">
107+
All technologies selected
108+
</p>
109+
),
110+
options:[
111+
{
112+
label:"Docker",
113+
value:"docker",
114+
icon:"/icon/docker.png",
115+
},
116+
{
117+
label:"Kubernetes",
118+
value:"kubernetes",
119+
icon:"/icon/k8s.png",
120+
},
121+
{
122+
label:"VS Code",
123+
value:"vscode",
124+
icon:"/icon/code.svg",
125+
},
126+
{
127+
label:"JetBrains",
128+
value:"jetbrains",
129+
icon:"/icon/intellij.svg",
130+
},
131+
{
132+
label:"Jupyter",
133+
value:"jupyter",
134+
icon:"/icon/jupyter.svg",
135+
},
136+
],
137+
},
138+
play:async({ canvasElement})=>{
139+
constcanvas=within(canvasElement);
140+
141+
// Open the combobox
142+
awaituserEvent.click(canvas.getByPlaceholderText("Select technology"));
143+
144+
// Verify that Docker option has an icon
145+
constdockerOption=canvas.getByRole("option",{name:/Docker/i});
146+
constdockerIcon=dockerOption.querySelector("img");
147+
awaitexpect(dockerIcon).toBeInTheDocument();
148+
awaitexpect(dockerIcon).toHaveAttribute("src","/icon/docker.png");
149+
150+
// Select Docker and verify icon appears in badge
151+
awaituserEvent.click(dockerOption);
152+
153+
// Find the Docker badge
154+
constdockerBadge=canvas
155+
.getByText("Docker")
156+
.closest('[role="button"]')?.parentElement;
157+
constbadgeIcon=dockerBadge?.querySelector("img");
158+
awaitexpect(badgeIcon).toBeInTheDocument();
159+
awaitexpect(badgeIcon).toHaveAttribute("src","/icon/docker.png");
160+
},
161+
};
162+
163+
exportconstMixedWithAndWithoutIcons:Story={
164+
args:{
165+
placeholder:"Select resource",
166+
options:[
167+
{
168+
label:"CPU",
169+
value:"cpu",
170+
icon:"/icon/memory.svg",
171+
},
172+
{
173+
label:"Memory",
174+
value:"memory",
175+
icon:"/icon/memory.svg",
176+
},
177+
{
178+
label:"Storage",
179+
value:"storage",
180+
},
181+
{
182+
label:"Network",
183+
value:"network",
184+
},
185+
],
186+
},
187+
play:async({ canvasElement})=>{
188+
constcanvas=within(canvasElement);
189+
190+
// Open the combobox
191+
awaituserEvent.click(canvas.getByPlaceholderText("Select resource"));
192+
193+
// Verify that CPU option has an icon
194+
constcpuOption=canvas.getByRole("option",{name:/CPU/i});
195+
constcpuIcon=cpuOption.querySelector("img");
196+
awaitexpect(cpuIcon).toBeInTheDocument();
197+
198+
// Verify that Storage option does not have an icon
199+
conststorageOption=canvas.getByRole("option",{name:/Storage/i});
200+
conststorageIcon=storageOption.querySelector("img");
201+
awaitexpect(storageIcon).not.toBeInTheDocument();
202+
203+
// Select both and verify badges
204+
awaituserEvent.click(cpuOption);
205+
awaituserEvent.click(storageOption);
206+
207+
// CPU badge should have icon
208+
constcpuBadge=canvas
209+
.getByText("CPU")
210+
.closest('[role="button"]')?.parentElement;
211+
constcpuBadgeIcon=cpuBadge?.querySelector("img");
212+
awaitexpect(cpuBadgeIcon).toBeInTheDocument();
213+
214+
// Storage badge should not have icon
215+
conststorageBadge=canvas
216+
.getByText("Storage")
217+
.closest('[role="button"]')?.parentElement;
218+
conststorageBadgeIcon=storageBadge?.querySelector("img");
219+
awaitexpect(storageBadgeIcon).not.toBeInTheDocument();
220+
},
221+
};
222+
223+
exportconstWithGroupedIcons:Story={
224+
args:{
225+
placeholder:"Select tools",
226+
groupBy:"category",
227+
options:[
228+
{
229+
label:"Docker",
230+
value:"docker",
231+
icon:"/icon/docker.png",
232+
category:"Containers",
233+
},
234+
{
235+
label:"Kubernetes",
236+
value:"kubernetes",
237+
icon:"/icon/k8s.png",
238+
category:"Containers",
239+
},
240+
{
241+
label:"VS Code",
242+
value:"vscode",
243+
icon:"/icon/code.svg",
244+
category:"IDEs",
245+
},
246+
{
247+
label:"JetBrains",
248+
value:"jetbrains",
249+
icon:"/icon/intellij.svg",
250+
category:"IDEs",
251+
},
252+
{
253+
label:"Zed",
254+
value:"zed",
255+
icon:"/icon/zed.svg",
256+
category:"IDEs",
257+
},
258+
],
259+
},
260+
play:async({ canvasElement})=>{
261+
constcanvas=within(canvasElement);
262+
263+
// Open the combobox
264+
awaituserEvent.click(canvas.getByPlaceholderText("Select tools"));
265+
266+
// Verify grouped options still have icons
267+
constdockerOption=canvas.getByRole("option",{name:/Docker/i});
268+
constdockerIcon=dockerOption.querySelector("img");
269+
awaitexpect(dockerIcon).toBeInTheDocument();
270+
awaitexpect(dockerIcon).toHaveAttribute("src","/icon/docker.png");
271+
272+
constvscodeOption=canvas.getByRole("option",{name:/VSCode/i});
273+
constvscodeIcon=vscodeOption.querySelector("img");
274+
awaitexpect(vscodeIcon).toBeInTheDocument();
275+
awaitexpect(vscodeIcon).toHaveAttribute("src","/icon/code.svg");
276+
277+
// Verify grouping headers are present
278+
awaitexpect(canvas.getByText("Containers")).toBeInTheDocument();
279+
awaitexpect(canvas.getByText("IDEs")).toBeInTheDocument();
280+
},
281+
};

‎site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* This component is based on multiple-selector
33
*@see {@link https://shadcnui-expansions.typeart.cc/docs/multiple-selector}
44
*/
5+
import{useTheme}from"@emotion/react";
56
import{CommandasCommandPrimitive,useCommandState}from"cmdk";
67
import{Badge}from"components/Badge/Badge";
78
import{
@@ -25,11 +26,13 @@ import {
2526
useRef,
2627
useState,
2728
}from"react";
29+
import{getExternalImageStylesFromUrl}from"theme/externalImages";
2830
import{cn}from"utils/cn";
2931

3032
exportinterfaceOption{
3133
value:string;
3234
label:string;
35+
icon?:string;
3336
disable?:boolean;
3437
/** fixed option that can't be removed. */
3538
fixed?:boolean;
@@ -204,6 +207,7 @@ export const MultiSelectCombobox = forwardRef<
204207
const[onScrollbar,setOnScrollbar]=useState(false);
205208
const[isLoading,setIsLoading]=useState(false);
206209
constdropdownRef=useRef<HTMLDivElement>(null);
210+
consttheme=useTheme();
207211

208212
const[selected,setSelected]=useState<Option[]>(
209213
arrayDefaultOptions??[],
@@ -487,7 +491,20 @@ export const MultiSelectCombobox = forwardRef<
487491
data-fixed={option.fixed}
488492
data-disabled={disabled||undefined}
489493
>
490-
{option.label}
494+
<divclassName="flex items-center gap-1">
495+
{option.icon&&(
496+
<img
497+
src={option.icon}
498+
alt=""
499+
className="size-icon-xs"
500+
css={getExternalImageStylesFromUrl(
501+
theme.externalImages,
502+
option.icon,
503+
)}
504+
/>
505+
)}
506+
{option.label}
507+
</div>
491508
<button
492509
type="button"
493510
data-testid="clear-option-button"
@@ -639,7 +656,20 @@ export const MultiSelectCombobox = forwardRef<
639656
"cursor-default text-content-disabled",
640657
)}
641658
>
642-
{option.label}
659+
<divclassName="flex items-center gap-2">
660+
{option.icon&&(
661+
<img
662+
src={option.icon}
663+
alt=""
664+
className="size-icon-sm"
665+
css={getExternalImageStylesFromUrl(
666+
theme.externalImages,
667+
option.icon,
668+
)}
669+
/>
670+
)}
671+
{option.label}
672+
</div>
643673
</CommandItem>
644674
);
645675
})}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp