|
4 | 4 | * See License.AGPL.txt in the project root for license information.
|
5 | 5 | */
|
6 | 6 |
|
7 |
| -import { useState } from "react"; |
| 7 | +import { FC, useCallback, useMemo, useState } from "react"; |
8 | 8 | import { useListOrganizationMembers } from "../../data/organizations/members-query";
|
9 |
| - |
10 | 9 | import type { OnboardingSettings_WelcomeMessage } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
|
11 |
| -import { Button } from "@podkit/buttons/Button"; |
12 |
| -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@podkit/dropdown/DropDown"; |
| 10 | +import { Combobox } from "../../prebuilds/configuration-input/Combobox"; |
| 11 | +import { ComboboxSelectedItem } from "../../prebuilds/configuration-input/ComboboxSelectedItem"; |
13 | 12 |
|
14 | 13 | type Props = {
|
15 | 14 | settings: OnboardingSettings_WelcomeMessage | undefined;
|
16 | 15 | setFeaturedMemberId: (featuredMemberId: string | undefined) => void;
|
17 | 16 | };
|
18 | 17 | export const OrgMemberAvatarInput = ({ settings, setFeaturedMemberId }: Props) => {
|
19 |
| - const { data: members } = useListOrganizationMembers(); |
20 |
| - |
| 18 | + const { data: members, isLoading } = useListOrganizationMembers(); |
| 19 | + const [searchTerm, setSearchTerm] = useState(""); |
21 | 20 | const [avatarUrl, setAvatarUrl] = useState<string | undefined>(settings?.featuredMemberResolvedAvatarUrl);
|
22 | 21 |
|
| 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], |
| 33 | + ); |
| 34 | + |
| 35 | + const getElements = useCallback(() => { |
| 36 | + const resetFilterItem = { |
| 37 | + id: "", |
| 38 | + element: <SuggestedMemberOption member={{ fullName: "Disable image" }} />, |
| 39 | + isSelectable: true, |
| 40 | + }; |
| 41 | + |
| 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]); |
| 59 | + |
23 | 60 | return (
|
24 |
| - <DropdownMenu> |
25 |
| - <DropdownMenuTrigger> |
26 |
| - <div className="flex flex-col justify-center items-center gap-2"> |
27 |
| - {avatarUrl ? ( |
28 |
| - <img src={avatarUrl} alt="" className="w-16 h-16 rounded-full" /> |
29 |
| - ) : ( |
30 |
| - <div className="w-16 h-16 rounded-full bg-[#EA71DE]" /> |
31 |
| - )} |
32 |
| - <Button variant="secondary" size="sm" type="button"> |
33 |
| - Change Photo |
34 |
| - </Button> |
35 |
| - </div> |
36 |
| - </DropdownMenuTrigger> |
37 |
| - <DropdownMenuContent> |
38 |
| - <DropdownMenuItem |
39 |
| - key="disabled" |
40 |
| - onClick={() => { |
41 |
| - setFeaturedMemberId(undefined); |
42 |
| - setAvatarUrl(undefined); |
43 |
| - }} |
| 61 | + <div className="flex flex-col items-center gap-4"> |
| 62 | + {avatarUrl ? ( |
| 63 | + <img src={avatarUrl} alt="" className="w-16 h-16 rounded-full" /> |
| 64 | + ) : ( |
| 65 | + <div className="w-16 h-16 rounded-full bg-[#EA71DE]" /> |
| 66 | + )} |
| 67 | + |
| 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" |
44 | 77 | >
|
45 |
| - <div className="flex items-center gap-2"> |
46 |
| - <div className="w-4 h-4 rounded-full bg-pk-surface-tertiary" /> |
47 |
| - Disable image |
48 |
| - </div> |
49 |
| - </DropdownMenuItem> |
50 |
| - {members?.map((member) => ( |
51 |
| - <DropdownMenuItem |
52 |
| - key={member.userId} |
53 |
| - onClick={() => { |
54 |
| - setFeaturedMemberId(member.userId); |
55 |
| - setAvatarUrl(member.avatarUrl); |
56 |
| - }} |
57 |
| - > |
58 |
| - <div className="flex items-center gap-2"> |
59 |
| - <img src={member.avatarUrl} alt={member.fullName} className="w-4 h-4 rounded-full" /> |
60 |
| - {member.fullName} |
61 |
| - </div> |
62 |
| - </DropdownMenuItem> |
63 |
| - ))} |
64 |
| - </DropdownMenuContent> |
65 |
| - </DropdownMenu> |
| 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, avatarUrl } = member; |
| 105 | + |
| 106 | + return ( |
| 107 | + <div className="flex flex-row items-center overflow-hidden" title={fullName} aria-label={`Member: ${fullName}`}> |
| 108 | + {avatarUrl ? ( |
| 109 | + <img src={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>} |
| 114 | + </div> |
66 | 115 | );
|
67 | 116 | };
|
0 commit comments