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" ;
88import { useListOrganizationMembers } from "../../data/organizations/members-query" ;
9-
109import 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
1713type Props = {
1814 settings : OnboardingSettings_WelcomeMessage | undefined ;
1915 setFeaturedMemberId : ( featuredMemberId : string | undefined ) => void ;
2016} ;
2117export 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