Skip to content

Commit 74ca266

Browse files
committed
A proper fix with existing prebuild list combobox
Tool: gitpod/catfood.gitpod.cloud
1 parent d238aa5 commit 74ca266

File tree

2 files changed

+85
-245
lines changed

2 files changed

+85
-245
lines changed

components/dashboard/src/teams/TeamOnboarding.tsx

Lines changed: 0 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@ import { LoadingState } from "@podkit/loading/LoadingState";
2727
import { Table, TableHeader, TableRow, TableHead, TableBody } from "@podkit/tables/Table";
2828
import { WelcomeMessageConfigurationField } from "./onboarding/WelcomeMessageConfigurationField";
2929
import { OrgMemberAvatarInput } from "./onboarding/OrgMemberAvatarInput";
30-
import { Popover, PopoverContent, PopoverTrigger } from "@podkit/popover/Popover";
31-
import { Button } from "@podkit/buttons/Button";
32-
import { Check, ChevronsUpDown } from "lucide-react";
33-
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@podkit/command/Command";
34-
import { cn } from "@podkit/lib/cn";
3530

3631
export type UpdateTeamSettingsOptions = {
3732
throwMutateError?: boolean;
@@ -176,177 +171,9 @@ export default function TeamOnboardingPage() {
176171
settings={settings?.onboardingSettings?.welcomeMessage}
177172
setFeaturedMemberId={setFeaturedMemberId}
178173
/>
179-
<ComboboxDemo />
180174

181175
<WelcomeMessageConfigurationField handleUpdateTeamSettings={handleUpdateTeamSettings} />
182176
</div>
183177
</OrgSettingsPage>
184178
);
185179
}
186-
187-
const frameworks = [
188-
{
189-
value: "next.js",
190-
label: "AAAANext.js",
191-
},
192-
{
193-
value: "sveltekit",
194-
label: "SvelteKit",
195-
},
196-
{
197-
value: "nuxt.js",
198-
label: "Nuxt.js",
199-
},
200-
{
201-
value: "remix",
202-
label: "Remix",
203-
},
204-
{
205-
value: "astro",
206-
label: "Astro",
207-
},
208-
{
209-
value: "gatsby",
210-
label: "Gatsby",
211-
},
212-
{
213-
value: "angular",
214-
label: "Angular",
215-
},
216-
{
217-
value: "ember",
218-
label: "Ember.js",
219-
},
220-
{
221-
value: "qwik",
222-
label: "Qwik",
223-
},
224-
{
225-
value: "solid",
226-
label: "SolidJS",
227-
},
228-
{
229-
value: "vite",
230-
label: "Vite",
231-
},
232-
{
233-
value: "eleventy",
234-
label: "Eleventy",
235-
},
236-
{
237-
value: "redwood",
238-
label: "RedwoodJS",
239-
},
240-
{
241-
value: "fresh",
242-
label: "Fresh",
243-
},
244-
{
245-
value: "nest",
246-
label: "NestJS",
247-
},
248-
{
249-
value: "vue",
250-
label: "Vue.js",
251-
},
252-
{
253-
value: "create-react-app",
254-
label: "Create React App",
255-
},
256-
{
257-
value: "preact",
258-
label: "Preact",
259-
},
260-
{
261-
value: "gridsome",
262-
label: "Gridsome",
263-
},
264-
{
265-
value: "blitz",
266-
label: "Blitz.js",
267-
},
268-
{
269-
value: "hydrogen",
270-
label: "Hydrogen",
271-
},
272-
{
273-
value: "remix-indie",
274-
label: "Remix Indie Stack",
275-
},
276-
{
277-
value: "expo",
278-
label: "Expo",
279-
},
280-
{
281-
value: "docusaurus",
282-
label: "Docusaurus",
283-
},
284-
{
285-
value: "react-native",
286-
label: "React Native",
287-
},
288-
{
289-
value: "t3-app",
290-
label: "Create T3 App",
291-
},
292-
{
293-
value: "ionic",
294-
label: "Ionic",
295-
},
296-
{
297-
value: "vitepress",
298-
label: "VitePress",
299-
},
300-
{
301-
value: "nextra",
302-
label: "Nextra",
303-
},
304-
{
305-
value: "adonis",
306-
label: "AdonisJS",
307-
},
308-
];
309-
310-
export function ComboboxDemo() {
311-
const [open, setOpen] = useState(false);
312-
const [value, setValue] = useState("");
313-
314-
return (
315-
<Popover open={open} onOpenChange={setOpen}>
316-
<PopoverTrigger asChild>
317-
<Button variant="outline" role="combobox" aria-expanded={open} className="w-[200px] justify-between">
318-
{value ? frameworks.find((framework) => framework.value === value)?.label : "Select framework..."}
319-
<ChevronsUpDown className="opacity-50" />
320-
</Button>
321-
</PopoverTrigger>
322-
<PopoverContent className="w-[200px] p-0">
323-
<Command>
324-
<CommandInput placeholder="Search framework..." className="h-9" />
325-
<CommandList>
326-
<CommandEmpty>No framework found.</CommandEmpty>
327-
<CommandGroup>
328-
{frameworks.map((framework) => (
329-
<CommandItem
330-
key={framework.value}
331-
value={framework.value}
332-
onSelect={(currentValue) => {
333-
setValue(currentValue === value ? "" : currentValue);
334-
setOpen(false);
335-
}}
336-
>
337-
{framework.label}
338-
<Check
339-
className={cn(
340-
"ml-auto",
341-
value === framework.value ? "opacity-100" : "opacity-0",
342-
)}
343-
/>
344-
</CommandItem>
345-
))}
346-
</CommandGroup>
347-
</CommandList>
348-
</Command>
349-
</PopoverContent>
350-
</Popover>
351-
);
352-
}

components/dashboard/src/teams/onboarding/OrgMemberAvatarInput.tsx

Lines changed: 85 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,58 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { useMemo, useState } from "react";
7+
import { FC, useCallback, useMemo, useState } from "react";
88
import { useListOrganizationMembers } from "../../data/organizations/members-query";
9-
109
import type { OnboardingSettings_WelcomeMessage } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
11-
import { Button } from "@podkit/buttons/Button";
12-
import { Popover, PopoverContent, PopoverTrigger } from "@podkit/popover/Popover";
13-
import { Check, ChevronsUpDown } from "lucide-react";
14-
import { Command, CommandGroup, CommandInput, CommandItem } from "@podkit/command/Command";
15-
import { cn } from "@podkit/lib/cn";
10+
import { Combobox } from "../../prebuilds/configuration-input/Combobox";
11+
import { ComboboxSelectedItem } from "../../prebuilds/configuration-input/ComboboxSelectedItem";
1612

1713
type Props = {
1814
settings: OnboardingSettings_WelcomeMessage | undefined;
1915
setFeaturedMemberId: (featuredMemberId: string | undefined) => void;
2016
};
2117
export const OrgMemberAvatarInput = ({ settings, setFeaturedMemberId }: Props) => {
22-
const { data: members } = useListOrganizationMembers();
23-
const [open, setOpen] = useState(false);
18+
const { data: members, isLoading } = useListOrganizationMembers();
19+
const [searchTerm, setSearchTerm] = useState("");
2420
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(settings?.featuredMemberResolvedAvatarUrl);
25-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26-
const [search, _setSearch] = useState<string | undefined>(undefined);
2721

28-
const allOptions = useMemo(
29-
() => [{ userId: "disabled", fullName: "Disable image", avatarUrl: undefined }, ...(members ?? [])],
30-
[members],
22+
const selectedMember = useMemo(() => {
23+
return members?.find((member) => member.avatarUrl === avatarUrl);
24+
}, [members, avatarUrl]);
25+
26+
const handleSelectionChange = useCallback(
27+
(selectedId: string) => {
28+
const member = members?.find((m) => m.userId === selectedId);
29+
setFeaturedMemberId(selectedId || undefined);
30+
setAvatarUrl(member?.avatarUrl);
31+
},
32+
[members, setFeaturedMemberId],
3133
);
3234

33-
const filteredOptions = useMemo(() => {
34-
return allOptions.filter((member) => {
35-
const fullName = member.fullName.toLowerCase();
36-
const searchTerms = search?.toLowerCase() ?? "";
37-
return fullName.includes(searchTerms);
38-
});
39-
}, [allOptions, search]);
35+
const getElements = useCallback(() => {
36+
const resetFilterItem = {
37+
id: "",
38+
element: <SuggestedMemberOption member={{ fullName: "Disable image" }} />,
39+
isSelectable: true,
40+
};
4041

41-
const selectedMember = allOptions.find(
42-
(member) => member.avatarUrl === avatarUrl || (avatarUrl === undefined && member.userId === "disabled"),
43-
);
42+
if (!members) {
43+
return [resetFilterItem];
44+
}
45+
46+
const filteredMembers = members.filter((member) =>
47+
member.fullName.toLowerCase().includes(searchTerm.toLowerCase().trim()),
48+
);
49+
50+
const result = filteredMembers.map((member) => ({
51+
id: member.userId,
52+
element: <SuggestedMemberOption member={member} />,
53+
isSelectable: true,
54+
}));
55+
if (searchTerm.length === 0) result.unshift(resetFilterItem);
56+
57+
return result;
58+
}, [members, searchTerm]);
4459

4560
return (
4661
<div className="flex flex-col items-center gap-4">
@@ -50,54 +65,52 @@ export const OrgMemberAvatarInput = ({ settings, setFeaturedMemberId }: Props) =
5065
<div className="w-16 h-16 rounded-full bg-[#EA71DE]" />
5166
)}
5267

53-
<Popover open={open} onOpenChange={setOpen}>
54-
<PopoverTrigger asChild>
55-
<Button variant="outline" role="combobox" aria-expanded={open} className="w-48 justify-between">
56-
{selectedMember?.fullName ?? "Select member..."}
57-
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
58-
</Button>
59-
</PopoverTrigger>
60-
<PopoverContent className="w-60 p-0">
61-
<Command shouldFilter={true}>
62-
<CommandInput
63-
// value={search}
64-
// onValueChange={(value) => setSearch(value)}
65-
className="h-9"
66-
placeholder="Search members..."
67-
/>
68-
<CommandGroup>
69-
{filteredOptions.map((member) => (
70-
<CommandItem
71-
key={member.userId}
72-
value={member.userId}
73-
onSelect={(newMemberId) => {
74-
setFeaturedMemberId(newMemberId === "disabled" ? undefined : newMemberId);
75-
setAvatarUrl(member.avatarUrl);
76-
setOpen(false);
77-
}}
78-
>
79-
{member.avatarUrl ? (
80-
<img
81-
src={member.avatarUrl}
82-
alt={member.fullName}
83-
className="w-4 h-4 rounded-full"
84-
/>
85-
) : (
86-
<div className="w-4 h-4 rounded-full bg-pk-surface-tertiary" />
87-
)}
88-
{member.fullName}
89-
<Check
90-
className={cn(
91-
"ml-auto h-4 w-4",
92-
selectedMember?.userId === member.userId ? "opacity-100" : "opacity-0",
93-
)}
94-
/>
95-
</CommandItem>
96-
))}
97-
</CommandGroup>
98-
</Command>
99-
</PopoverContent>
100-
</Popover>
68+
<div className="w-48">
69+
<Combobox
70+
getElements={getElements}
71+
initialValue={selectedMember?.userId}
72+
onSelectionChange={handleSelectionChange}
73+
disabled={isLoading}
74+
loading={isLoading}
75+
onSearchChange={setSearchTerm}
76+
dropDownClassName="text-pk-content-primary"
77+
>
78+
<ComboboxSelectedItem
79+
htmlTitle="Member"
80+
title={<div className="truncate">{selectedMember?.fullName ?? "Select member..."}</div>}
81+
titleClassName="text-sm font-normal text-pk-content-primary"
82+
loading={isLoading}
83+
icon={
84+
selectedMember?.avatarUrl ? (
85+
<img src={selectedMember.avatarUrl} alt="" className="w-4 h-4 rounded-full" />
86+
) : (
87+
<div className="w-4 h-4 rounded-full bg-pk-content-tertiary" />
88+
)
89+
}
90+
/>
91+
</Combobox>
92+
</div>
93+
</div>
94+
);
95+
};
96+
97+
type SuggestedMemberOptionProps = {
98+
member: {
99+
fullName: string;
100+
avatarUrl?: string;
101+
};
102+
};
103+
const SuggestedMemberOption: FC<SuggestedMemberOptionProps> = ({ member }) => {
104+
const { fullName } = member;
105+
106+
return (
107+
<div className="flex flex-row items-center overflow-hidden" title={fullName} aria-label={`Member: ${fullName}`}>
108+
{member.avatarUrl ? (
109+
<img src={member.avatarUrl} alt="" className="w-4 h-4 rounded-full mr-2" />
110+
) : (
111+
<div className="w-4 h-4 rounded-full mr-2 bg-pk-content-tertiary" />
112+
)}
113+
{fullName && <span className="text-sm whitespace-nowrap">{fullName}</span>}
101114
</div>
102115
);
103116
};

0 commit comments

Comments
 (0)