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

Commit52c4b61

Browse files
authored
feat: add search to parameter dropdowns (#18729)
1 parentdad033e commit52c4b61

File tree

9 files changed

+178
-138
lines changed

9 files changed

+178
-138
lines changed

‎.github/.linkspector.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ ignorePatterns:
2525
-pattern:"docs.github.com"
2626
-pattern:"claude.ai"
2727
-pattern:"splunk.com"
28+
-pattern:"stackoverflow.com/questions"
2829
aliveStatusCodes:
2930
-200

‎docs/about/contributing/frontend.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ All UI-related code is in the `site` folder. Key directories include:
6666
-**util** - Helper functions that can be used across the application
6767
-**static** - Static assets like images, fonts, icons, etc
6868

69+
Do not use barrel files. Imports should be directly from the file that defines
70+
the value.
71+
6972
##Routing
7073

7174
We use[react-router](https://reactrouter.com/en/main) as our routing engine.

‎site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"lint:check":" biome lint --error-on-warnings .",
1818
"lint:circular-deps":"dpdm --no-tree --no-warning -T ./src/App.tsx",
1919
"lint:knip":"knip",
20-
"lint:fix":"biome lint --error-on-warnings --write . && knip --fix",
20+
"lint:fix":"biome lint --error-on-warnings --write . && knip --fix",
2121
"lint:types":"tsc -p .",
2222
"playwright:install":"playwright install --with-deps chromium",
2323
"playwright:test":"playwright test --config=e2e/playwright.config.ts",

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

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,35 @@ import { expect, screen, userEvent, waitFor, within } from "@storybook/test";
33
import{useState}from"react";
44
import{Combobox}from"./Combobox";
55

6-
constoptions=["Option 1","Option 2","Option 3","Another Option"];
6+
constsimpleOptions=["Go","Gleam","Kotlin","Rust"];
77

8-
constComboboxWithHooks=()=>{
8+
constadvancedOptions=[
9+
{
10+
displayName:"Go",
11+
value:"go",
12+
icon:"/icon/go.svg",
13+
},
14+
{
15+
displayName:"Gleam",
16+
value:"gleam",
17+
icon:"https://github.com/gleam-lang.png",
18+
},
19+
{
20+
displayName:"Kotlin",
21+
value:"kotlin",
22+
description:"Kotlin 2.1, OpenJDK 24, gradle",
23+
icon:"/icon/kotlin.svg",
24+
},
25+
{
26+
displayName:"Rust",
27+
value:"rust",
28+
icon:"/icon/rust.svg",
29+
},
30+
]asconst;
31+
32+
constComboboxWithHooks=({
33+
options=advancedOptions,
34+
}:{options?:React.ComponentProps<typeofCombobox>["options"]})=>{
935
const[value,setValue]=useState("");
1036
const[open,setOpen]=useState(false);
1137
const[inputValue,setInputValue]=useState("");
@@ -34,17 +60,21 @@ const ComboboxWithHooks = () => {
3460
constmeta:Meta<typeofCombobox>={
3561
title:"components/Combobox",
3662
component:Combobox,
63+
args:{options:advancedOptions},
3764
};
3865

3966
exportdefaultmeta;
4067
typeStory=StoryObj<typeofCombobox>;
4168

42-
exportconstDefault:Story={
43-
render:()=><ComboboxWithHooks/>,
69+
exportconstDefault:Story={};
70+
71+
exportconstSimpleOptions:Story={
72+
args:{
73+
options:simpleOptions,
74+
},
4475
};
4576

4677
exportconstOpenCombobox:Story={
47-
render:()=><ComboboxWithHooks/>,
4878
play:async({ canvasElement})=>{
4979
constcanvas=within(canvasElement);
5080
awaituserEvent.click(canvas.getByRole("button"));
@@ -58,11 +88,7 @@ export const SelectOption: Story = {
5888
play:async({ canvasElement})=>{
5989
constcanvas=within(canvasElement);
6090
awaituserEvent.click(canvas.getByRole("button"));
61-
awaituserEvent.click(screen.getByText("Option 1"));
62-
63-
awaitwaitFor(()=>
64-
expect(canvas.getByRole("button")).toHaveTextContent("Option 1"),
65-
);
91+
awaituserEvent.click(screen.getByText("Go"));
6692
},
6793
};
6894

@@ -71,19 +97,13 @@ export const SearchAndFilter: Story = {
7197
play:async({ canvasElement})=>{
7298
constcanvas=within(canvasElement);
7399
awaituserEvent.click(canvas.getByRole("button"));
74-
awaituserEvent.type(screen.getByRole("combobox"),"Another");
75-
awaituserEvent.click(
76-
screen.getByRole("option",{name:"Another Option"}),
77-
);
78-
100+
awaituserEvent.type(screen.getByRole("combobox"),"r");
79101
awaitwaitFor(()=>{
80102
expect(
81-
screen.getByRole("option",{name:"Another Option"}),
82-
).toBeInTheDocument();
83-
expect(
84-
screen.queryByRole("option",{name:"Option 1"}),
103+
screen.queryByRole("option",{name:"Kotlin"}),
85104
).not.toBeInTheDocument();
86105
});
106+
awaituserEvent.click(screen.getByRole("option",{name:"Rust"}));
87107
},
88108
};
89109

@@ -92,16 +112,11 @@ export const EnterCustomValue: Story = {
92112
play:async({ canvasElement})=>{
93113
constcanvas=within(canvasElement);
94114
awaituserEvent.click(canvas.getByRole("button"));
95-
awaituserEvent.type(screen.getByRole("combobox"),"Custom Value{enter}");
96-
97-
awaitwaitFor(()=>
98-
expect(canvas.getByRole("button")).toHaveTextContent("Custom Value"),
99-
);
115+
awaituserEvent.type(screen.getByRole("combobox"),"Swift{enter}");
100116
},
101117
};
102118

103119
exportconstNoResults:Story={
104-
render:()=><ComboboxWithHooks/>,
105120
play:async({ canvasElement})=>{
106121
constcanvas=within(canvasElement);
107122
awaituserEvent.click(canvas.getByRole("button"));
@@ -120,10 +135,11 @@ export const ClearSelectedOption: Story = {
120135
constcanvas=within(canvasElement);
121136

122137
awaituserEvent.click(canvas.getByRole("button"));
138+
// const goOption = screen.getByText("Go");
123139
// First select an option
124-
awaituserEvent.click(screen.getByRole("option",{name:"Option 1"}));
140+
awaituserEvent.click(awaitscreen.findByRole("option",{name:"Go"}));
125141
// Then clear it by selecting it again
126-
awaituserEvent.click(screen.getByRole("option",{name:"Option 1"}));
142+
awaituserEvent.click(awaitscreen.findByRole("option",{name:"Go"}));
127143

128144
awaitwaitFor(()=>
129145
expect(canvas.getByRole("button")).toHaveTextContent("Select option"),

‎site/src/components/Combobox/Combobox.tsx

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import{Avatar}from"components/Avatar/Avatar";
12
import{Button}from"components/Button/Button";
23
import{
34
Command,
@@ -12,22 +13,36 @@ import {
1213
PopoverContent,
1314
PopoverTrigger,
1415
}from"components/Popover/Popover";
16+
import{
17+
Tooltip,
18+
TooltipContent,
19+
TooltipProvider,
20+
TooltipTrigger,
21+
}from"components/Tooltip/Tooltip";
1522
import{Check,ChevronDown,CornerDownLeft}from"lucide-react";
16-
importtype{FC,KeyboardEventHandler}from"react";
23+
import{Info}from"lucide-react";
24+
import{typeFC,typeKeyboardEventHandler,useState}from"react";
1725
import{cn}from"utils/cn";
1826

1927
interfaceComboboxProps{
2028
value:string;
21-
options?:readonlystring[];
29+
options?:Readonly<Array<string|ComboboxOption>>;
2230
placeholder?:string;
23-
open:boolean;
24-
onOpenChange:(open:boolean)=>void;
25-
inputValue:string;
26-
onInputChange:(value:string)=>void;
31+
open?:boolean;
32+
onOpenChange?:(open:boolean)=>void;
33+
inputValue?:string;
34+
onInputChange?:(value:string)=>void;
2735
onKeyDown?:KeyboardEventHandler<HTMLInputElement>;
2836
onSelect:(value:string)=>void;
2937
}
3038

39+
typeComboboxOption={
40+
icon?:string;
41+
displayName:string;
42+
value:string;
43+
description?:string;
44+
};
45+
3146
exportconstCombobox:FC<ComboboxProps>=({
3247
value,
3348
options=[],
@@ -39,16 +54,37 @@ export const Combobox: FC<ComboboxProps> = ({
3954
onKeyDown,
4055
onSelect,
4156
})=>{
57+
const[managedOpen,setManagedOpen]=useState(false);
58+
const[managedInputValue,setManagedInputValue]=useState("");
59+
60+
constoptionsMap=newMap<string,ComboboxOption>(
61+
options.map((option)=>
62+
typeofoption==="string"
63+
?[option,{displayName:option,value:option}]
64+
:[option.value,option],
65+
),
66+
);
67+
constoptionObjects=[...optionsMap.values()];
68+
constshowIcons=optionObjects.some((it)=>it.icon);
69+
70+
constisOpen=open??managedOpen;
71+
4272
return(
43-
<Popoveropen={open}onOpenChange={onOpenChange}>
73+
<Popover
74+
open={isOpen}
75+
onOpenChange={(newOpen)=>{
76+
setManagedOpen(newOpen);
77+
onOpenChange?.(newOpen);
78+
}}
79+
>
4480
<PopoverTriggerasChild>
4581
<Button
4682
variant="outline"
47-
aria-expanded={open}
83+
aria-expanded={isOpen}
4884
className="w-72 justify-between group"
4985
>
5086
<spanclassName={cn(!value&&"text-content-secondary")}>
51-
{value||placeholder}
87+
{optionsMap.get(value)?.displayName||value||placeholder}
5288
</span>
5389
<ChevronDownclassName="size-icon-sm text-content-secondary group-hover:text-content-primary"/>
5490
</Button>
@@ -57,8 +93,11 @@ export const Combobox: FC<ComboboxProps> = ({
5793
<Command>
5894
<CommandInput
5995
placeholder="Search or enter custom value"
60-
value={inputValue}
61-
onValueChange={onInputChange}
96+
value={inputValue??managedInputValue}
97+
onValueChange={(newValue)=>{
98+
setManagedInputValue(newValue);
99+
onInputChange?.(newValue);
100+
}}
62101
onKeyDown={onKeyDown}
63102
/>
64103
<CommandList>
@@ -70,18 +109,40 @@ export const Combobox: FC<ComboboxProps> = ({
70109
</span>
71110
</CommandEmpty>
72111
<CommandGroup>
73-
{options.map((option)=>(
112+
{optionObjects.map((option)=>(
74113
<CommandItem
75-
key={option}
76-
value={option}
114+
key={option.value}
115+
value={option.value}
116+
keywords={[option.displayName]}
77117
onSelect={(currentValue)=>{
78118
onSelect(currentValue===value ?"" :currentValue);
79119
}}
80120
>
81-
{option}
82-
{value===option&&(
83-
<CheckclassName="size-icon-sm ml-auto"/>
121+
{showIcons&&(
122+
<Avatar
123+
size="sm"
124+
src={option.icon}
125+
fallback={option.value}
126+
/>
84127
)}
128+
{option.displayName}
129+
<divclassName="flex flex-row items-center ml-auto gap-1">
130+
{value===option.value&&(
131+
<CheckclassName="size-icon-sm"/>
132+
)}
133+
{option.description&&(
134+
<TooltipProviderdelayDuration={100}>
135+
<Tooltip>
136+
<TooltipTriggerasChild>
137+
<InfoclassName="w-3.5 h-3.5 text-content-secondary"/>
138+
</TooltipTrigger>
139+
<TooltipContentside="right"sideOffset={10}>
140+
{option.description}
141+
</TooltipContent>
142+
</Tooltip>
143+
</TooltipProvider>
144+
)}
145+
</div>
85146
</CommandItem>
86147
))}
87148
</CommandGroup>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ export const MultiSelectCombobox = forwardRef<
617617
}}
618618
>
619619
{isLoading ?(
620-
<>{loadingIndicator}</>
620+
loadingIndicator
621621
) :(
622622
<>
623623
{EmptyItem()}

‎site/src/modules/workspaces/DynamicParameter/DynamicParameter.stories.tsx

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,48 @@ export const Dropdown: Story = {
5656
type:"string",
5757
options:[
5858
{
59-
name:"Option 1",
60-
value:{valid:true,value:"option1"},
61-
description:"this is option 1",
62-
icon:"",
59+
name:"Nissa, Worldsoul Speaker",
60+
value:{valid:true,value:"nissa"},
61+
description:
62+
"Zendikar still seems so far off, but Chandra is my home.",
63+
icon:"/emojis/1f7e2.png",
6364
},
6465
{
65-
name:"Option 2",
66-
value:{valid:true,value:"option2"},
67-
description:"this is option 2",
68-
icon:"",
66+
name:"Canopy Spider",
67+
value:{valid:true,value:"spider"},
68+
description:
69+
"It keeps the upper reaches of the forest free of every menace . . . except for the spider itself.",
70+
icon:"/emojis/1f7e2.png",
6971
},
7072
{
71-
name:"Option 3",
72-
value:{valid:true,value:"option3"},
73-
description:"this is option 3",
74-
icon:"",
73+
name:"Ajani, Nacatl Pariah",
74+
value:{valid:true,value:"ajani"},
75+
description:"His pride denied him; his brother did not.",
76+
icon:"/emojis/26aa.png",
77+
},
78+
{
79+
name:"Glowing Anemone",
80+
value:{valid:true,value:"anemone"},
81+
description:"Beautiful to behold, terrible to be held.",
82+
icon:"/emojis/1f535.png",
83+
},
84+
{
85+
name:"Springmantle Cleric",
86+
value:{valid:true,value:"cleric"},
87+
description:"Hope and courage bloom in her wake.",
88+
icon:"/emojis/1f7e2.png",
89+
},
90+
{
91+
name:"Aegar, the Freezing Flame",
92+
value:{valid:true,value:"aegar"},
93+
description:
94+
"Though Phyrexian machines could adapt to extremes of heat or cold, they never figured out how to adapt to both at once.",
95+
icon:"/emojis/1f308.png",
7596
},
7697
],
98+
styling:{
99+
placeholder:"Select a creature",
100+
},
77101
},
78102
},
79103
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp