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

Commit71738f6

Browse files
feat(site): support icon and description in preset (#19063)
## DescriptionThis PR updates the `CreateWorkspacePageView` to use the `Combobox`React component instead of `SelectFilter` for the Preset selection.## Changes* Updated `CreateWorkspacePageView` to use the `Combobox` component inplace of `SelectFilter`.* Modified the `Combobox` component to render preset icons using`ExternalImage` instead of `Avatar`.<img width="2172" height="1138" alt="Screenshot 2025-07-29 at 12 27 14"src="https://github.com/user-attachments/assets/2ef8342f-7927-4430-bf87-bc93c47d2980"/><img width="2176" height="1112" alt="Screenshot 2025-07-29 at 12 27 21"src="https://github.com/user-attachments/assets/863089a6-dcfd-46ed-8b85-68838ee04f28"/>Follow-up from:#18977---------Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
1 parent219d1b4 commit71738f6

File tree

7 files changed

+75
-72
lines changed

7 files changed

+75
-72
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ export const SearchAndFilter: Story = {
103103
screen.queryByRole("option",{name:"Kotlin"}),
104104
).not.toBeInTheDocument();
105105
});
106-
awaituserEvent.click(screen.getByRole("option",{name:"Rust"}));
106+
// Accessible name includes both image alt text and text content: "Rust Rust"
107+
awaituserEvent.click(screen.getByRole("option",{name:"Rust Rust"}));
107108
},
108109
};
109110

@@ -137,9 +138,11 @@ export const ClearSelectedOption: Story = {
137138
awaituserEvent.click(canvas.getByRole("button"));
138139
// const goOption = screen.getByText("Go");
139140
// First select an option
140-
awaituserEvent.click(awaitscreen.findByRole("option",{name:"Go"}));
141+
// Accessible name includes both image alt text and text content: "Go Go"
142+
awaituserEvent.click(awaitscreen.findByRole("option",{name:"Go Go"}));
141143
// Then clear it by selecting it again
142-
awaituserEvent.click(awaitscreen.findByRole("option",{name:"Go"}));
144+
// Accessible name includes both image alt text and text content: "Go Go"
145+
awaituserEvent.click(awaitscreen.findByRole("option",{name:"Go Go"}));
143146

144147
awaitwaitFor(()=>
145148
expect(canvas.getByRole("button")).toHaveTextContent("Select option"),

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

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import{Avatar}from"components/Avatar/Avatar";
21
import{Button}from"components/Button/Button";
32
import{
43
Command,
@@ -23,6 +22,7 @@ import { Check, ChevronDown, CornerDownLeft } from "lucide-react";
2322
import{Info}from"lucide-react";
2423
import{typeFC,typeKeyboardEventHandler,useState}from"react";
2524
import{cn}from"utils/cn";
25+
import{ExternalImage}from"../ExternalImage/ExternalImage";
2626

2727
interfaceComboboxProps{
2828
value:string;
@@ -69,27 +69,26 @@ export const Combobox: FC<ComboboxProps> = ({
6969

7070
constisOpen=open??managedOpen;
7171

72+
consthandleOpenChange=(newOpen:boolean)=>{
73+
setManagedOpen(newOpen);
74+
onOpenChange?.(newOpen);
75+
};
76+
7277
return(
73-
<Popover
74-
open={isOpen}
75-
onOpenChange={(newOpen)=>{
76-
setManagedOpen(newOpen);
77-
onOpenChange?.(newOpen);
78-
}}
79-
>
78+
<Popoveropen={isOpen}onOpenChange={handleOpenChange}>
8079
<PopoverTriggerasChild>
8180
<Button
8281
variant="outline"
8382
aria-expanded={isOpen}
84-
className="w-72 justify-between group"
83+
className="w-full justify-between group"
8584
>
8685
<spanclassName={cn(!value&&"text-content-secondary")}>
8786
{optionsMap.get(value)?.displayName||value||placeholder}
8887
</span>
8988
<ChevronDownclassName="size-icon-sm text-content-secondary group-hover:text-content-primary"/>
9089
</Button>
9190
</PopoverTrigger>
92-
<PopoverContentclassName="w-72">
91+
<PopoverContentclassName="w-[var(--radix-popover-trigger-width)]">
9392
<Command>
9493
<CommandInput
9594
placeholder="Search or enter custom value"
@@ -116,15 +115,21 @@ export const Combobox: FC<ComboboxProps> = ({
116115
keywords={[option.displayName]}
117116
onSelect={(currentValue)=>{
118117
onSelect(currentValue===value ?"" :currentValue);
118+
// Close the popover after selection
119+
handleOpenChange(false);
119120
}}
120121
>
121-
{showIcons&&(
122-
<Avatar
123-
size="sm"
124-
src={option.icon}
125-
fallback={option.value}
126-
/>
127-
)}
122+
{showIcons&&
123+
(option.icon ?(
124+
<ExternalImage
125+
className="w-4 h-4 object-contain"
126+
src={option.icon}
127+
alt={option.displayName}
128+
/>
129+
) :(
130+
/* Placeholder for missing icon to maintain layout consistency */
131+
<divclassName="w-4 h-4"></div>
132+
))}
128133
{option.displayName}
129134
<divclassName="flex flex-row items-center ml-auto gap-1">
130135
{value===option.value&&(

‎site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx‎

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import{action}from"@storybook/addon-actions";
22
importtype{Meta,StoryObj}from"@storybook/react";
3+
import{expect,screen,waitFor}from"@storybook/test";
34
import{within}from"@testing-library/react";
45
importuserEventfrom"@testing-library/user-event";
56
import{chromatic}from"testHelpers/chromatic";
@@ -129,8 +130,8 @@ export const PresetsButNoneSelected: Story = {
129130
{
130131
ID:"preset-1",
131132
Name:"Preset 1",
132-
Description:"",
133-
Icon:"",
133+
Description:"Preset 1 description",
134+
Icon:"/emojis/0031-fe0f-20e3.png",
134135
Default:false,
135136
Parameters:[
136137
{
@@ -143,9 +144,8 @@ export const PresetsButNoneSelected: Story = {
143144
{
144145
ID:"preset-2",
145146
Name:"Preset 2",
146-
Description:
147-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse imperdiet ultricies massa, eu dapibus ex fermentum ac.",
148-
Icon:"/emojis/1f60e.png",
147+
Description:"Preset 2 description",
148+
Icon:"/emojis/0032-fe0f-20e3.png",
149149
Default:false,
150150
Parameters:[
151151
{
@@ -165,21 +165,12 @@ export const PresetsButNoneSelected: Story = {
165165
};
166166

167167
exportconstPresetSelected:Story={
168-
args:PresetsButNoneSelected.args,
169-
play:async({ canvasElement})=>{
170-
constcanvas=within(canvasElement);
171-
awaituserEvent.click(canvas.getByLabelText("Preset"));
172-
awaituserEvent.click(canvas.getByText("Preset 1"));
173-
},
174-
};
175-
176-
exportconstPresetSelectedWithHiddenParameters:Story={
177168
args:PresetsButNoneSelected.args,
178169
play:async({ canvasElement})=>{
179170
constcanvas=within(canvasElement);
180171
// Select a preset
181-
awaituserEvent.click(canvas.getByLabelText("Preset"));
182-
awaituserEvent.click(canvas.getByText("Preset 1"));
172+
awaituserEvent.click(canvas.getByRole("button",{name:"None"}));
173+
awaituserEvent.click(screen.getByText("Preset 1"));
183174
},
184175
};
185176

@@ -188,8 +179,8 @@ export const PresetSelectedWithVisibleParameters: Story = {
188179
play:async({ canvasElement})=>{
189180
constcanvas=within(canvasElement);
190181
// Select a preset
191-
awaituserEvent.click(canvas.getByLabelText("Preset"));
192-
awaituserEvent.click(canvas.getByText("Preset 1"));
182+
awaituserEvent.click(canvas.getByRole("button",{name:"None"}));
183+
awaituserEvent.click(screen.getByText("Preset 1"));
193184
// Toggle off the show preset parameters switch
194185
awaituserEvent.click(canvas.getByLabelText("Show preset parameters"));
195186
},
@@ -201,16 +192,12 @@ export const PresetReselected: Story = {
201192
constcanvas=within(canvasElement);
202193

203194
// First selection of Preset 1
204-
awaituserEvent.click(canvas.getByLabelText("Preset"));
205-
awaituserEvent.click(
206-
canvas.getByText("Preset 1",{selector:".MuiMenuItem-root"}),
207-
);
195+
awaituserEvent.click(canvas.getByRole("button",{name:"None"}));
196+
awaituserEvent.click(screen.getByText("Preset 1"));
208197

209198
// Reselect the same preset
210-
awaituserEvent.click(canvas.getByLabelText("Preset"));
211-
awaituserEvent.click(
212-
canvas.getByText("Preset 1",{selector:".MuiMenuItem-root"}),
213-
);
199+
awaituserEvent.click(canvas.getByRole("button",{name:"Preset 1"}));
200+
awaituserEvent.click(canvas.getByText("Preset 1"));
214201
},
215202
};
216203

@@ -230,12 +217,11 @@ export const PresetNoneSelected: Story = {
230217
constcanvas=within(canvasElement);
231218

232219
// First select a preset to set the field value
233-
awaituserEvent.click(canvas.getByLabelText("Preset"));
234-
awaituserEvent.click(canvas.getByText("Preset 1"));
220+
awaituserEvent.click(canvas.getByRole("button",{name:"None"}));
221+
awaituserEvent.click(screen.getByText("Preset 1"));
235222

236223
// Then select "None" to unset the field value
237-
awaituserEvent.click(canvas.getByLabelText("Preset"));
238-
awaituserEvent.click(canvas.getByText("None"));
224+
awaituserEvent.click(screen.getByText("None"));
239225

240226
// Fill in required fields and submit to test the API call
241227
awaituserEvent.type(
@@ -260,8 +246,8 @@ export const PresetsWithDefault: Story = {
260246
{
261247
ID:"preset-1",
262248
Name:"Preset 1",
263-
Icon:"",
264-
Description:"",
249+
Description:"Preset 1 description",
250+
Icon:"/emojis/0031-fe0f-20e3.png",
265251
Default:false,
266252
Parameters:[
267253
{
@@ -274,9 +260,8 @@ export const PresetsWithDefault: Story = {
274260
{
275261
ID:"preset-2",
276262
Name:"Preset 2",
277-
Icon:"/emojis/1f60e.png",
278-
Description:
279-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse imperdiet ultricies massa, eu dapibus ex fermentum ac.",
263+
Description:"Preset 2 description",
264+
Icon:"/emojis/0032-fe0f-20e3.png",
280265
Default:true,
281266
Parameters:[
282267
{
@@ -295,6 +280,10 @@ export const PresetsWithDefault: Story = {
295280
},
296281
play:async({ canvasElement})=>{
297282
constcanvas=within(canvasElement);
283+
// Should have the default preset listed first
284+
awaitwaitFor(()=>
285+
expect(canvas.getByRole("button",{name:"Preset 2 (Default)"})),
286+
);
298287
// Wait for the switch to be available since preset parameters are populated asynchronously
299288
awaitcanvas.findByLabelText("Show preset parameters");
300289
// Toggle off the show preset parameters switch

‎site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Alert } from "components/Alert/Alert";
66
import{ErrorAlert}from"components/Alert/ErrorAlert";
77
import{Avatar}from"components/Avatar/Avatar";
88
import{Button}from"components/Button/Button";
9-
import{SelectFilter}from"components/Filter/SelectFilter";
9+
import{Combobox}from"components/Combobox/Combobox";
1010
import{
1111
FormFields,
1212
FormFooter,
@@ -158,16 +158,18 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
158158
);
159159

160160
const[presetOptions,setPresetOptions]=useState([
161-
{label:"None",value:""},
161+
{displayName:"None",value:"undefined",icon:"",description:""},
162162
]);
163163
const[selectedPresetIndex,setSelectedPresetIndex]=useState(0);
164164
// Build options and keep default label/value in sync
165165
useEffect(()=>{
166166
constoptions=[
167-
{label:"None",value:""},
168-
...presets.map((p)=>({
169-
label:p.Default ?`${p.Name} (Default)` :p.Name,
170-
value:p.ID,
167+
{displayName:"None",value:"undefined",icon:"",description:""},
168+
...presets.map((preset)=>({
169+
displayName:preset.Default ?`${preset.Name} (Default)` :preset.Name,
170+
value:preset.ID,
171+
icon:preset.Icon,
172+
description:preset.Description,
171173
})),
172174
];
173175
setPresetOptions(options);
@@ -392,25 +394,29 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
392394
</Stack>
393395
<Stackdirection="column"spacing={2}>
394396
<Stackdirection="row"spacing={2}>
395-
<SelectFilter
396-
label="Preset"
397+
<Combobox
398+
value={
399+
presetOptions[selectedPresetIndex]?.displayName||""
400+
}
397401
options={presetOptions}
398-
onSelect={(option)=>{
402+
placeholder="Select a preset"
403+
onSelect={(value)=>{
399404
constindex=presetOptions.findIndex(
400-
(preset)=>preset.value===option?.value,
405+
(preset)=>preset.value===value,
401406
);
402407
if(index===-1){
403408
return;
404409
}
405410
setSelectedPresetIndex(index);
406411
form.setFieldValue(
407412
"template_version_preset_id",
408-
// Empty string is equivalent to using None
409-
option?.value==="" ?undefined :option?.value,
413+
// "undefined" string is equivalent to using None option
414+
// Combobox requires a value in order to correctly highlight the None option
415+
presetOptions[index].value==="undefined"
416+
?undefined
417+
:presetOptions[index].value,
410418
);
411419
}}
412-
placeholder="Select a preset"
413-
selectedOption={presetOptions[selectedPresetIndex]}
414420
/>
415421
</Stack>
416422
{/* Only show the preset parameter visibility toggle if preset parameters are actually being modified, otherwise it has no effect. */}

‎site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
215215
)}
216216
<divclassName="flex flex-col gap-7">
217217
<divclassName="flex flex-row pt-8 gap-2 justify-between items-start">
218-
<divclassName="grid items-center gap-1">
218+
<divclassName="grid items-center gap-1 w-72">
219219
<LabelclassName="text-sm"htmlFor={`${id}-idp-org-name`}>
220220
IdP organization name
221221
</Label>

‎site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export const IdpGroupSyncForm: FC<IdpGroupSyncFormProps> = ({
219219
</span>
220220
</div>
221221
<divclassName="flex flex-row gap-2 justify-between items-start">
222-
<divclassName="grid items-center gap-1">
222+
<divclassName="grid items-center gap-1 w-72">
223223
<LabelclassName="text-sm"htmlFor={`${id}-idp-group-name`}>
224224
IdP group name
225225
</Label>

‎site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export const IdpRoleSyncForm: FC<IdpRoleSyncFormProps> = ({
159159
<pclassName="text-content-danger text-sm m-0">{form.errors.field}</p>
160160
)}
161161
<divclassName="flex flex-row gap-2 justify-between items-start">
162-
<divclassName="grid items-center gap-1">
162+
<divclassName="grid items-center gap-1 w-72">
163163
<LabelclassName="text-sm"htmlFor={`${id}-idp-role-name`}>
164164
IdP role name
165165
</Label>

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp