@@ -4,11 +4,12 @@ import {
44 CaretDownIcon ,
55 CheckIcon ,
66 PlusIcon ,
7+ SpinnerGapIcon ,
78 UserIcon ,
89 UsersIcon ,
910} from '@phosphor-icons/react' ;
1011import { useRouter } from 'next/navigation' ;
11- import * as React from 'react' ;
12+ import { useCallback , useState } from 'react' ;
1213import { CreateOrganizationDialog } from '@/components/organizations/create-organization-dialog' ;
1314import { Avatar , AvatarFallback , AvatarImage } from '@/components/ui/avatar' ;
1415import { Button } from '@/components/ui/button' ;
@@ -32,19 +33,43 @@ const getOrganizationInitials = (name: string) => {
3233 . slice ( 0 , 2 ) ;
3334} ;
3435
36+ function filterOrganizations < T extends { name : string ; slug ?: string | null } > (
37+ orgs : T [ ] | undefined ,
38+ query : string
39+ ) : T [ ] {
40+ if ( ! orgs || orgs . length === 0 ) {
41+ return [ ] ;
42+ }
43+ if ( ! query ) {
44+ return orgs ;
45+ }
46+ const q = query . toLowerCase ( ) ;
47+ const filtered : T [ ] = [ ] ;
48+ for ( const org of orgs ) {
49+ const nameMatch = org . name . toLowerCase ( ) . includes ( q ) ;
50+ const slugMatch = org . slug ? org . slug . toLowerCase ( ) . includes ( q ) : false ;
51+ if ( nameMatch || slugMatch ) {
52+ filtered . push ( org ) ;
53+ }
54+ }
55+ return filtered ;
56+ }
57+
3558export function OrganizationSelector ( ) {
3659 const {
3760 organizations,
3861 activeOrganization,
3962 isLoading,
4063 setActiveOrganization,
4164 isSettingActiveOrganization,
65+ hasError,
4266 } = useOrganizations ( ) ;
4367 const router = useRouter ( ) ;
44- const [ isOpen , setIsOpen ] = React . useState ( false ) ;
45- const [ showCreateDialog , setShowCreateDialog ] = React . useState ( false ) ;
68+ const [ isOpen , setIsOpen ] = useState ( false ) ;
69+ const [ showCreateDialog , setShowCreateDialog ] = useState ( false ) ;
70+ const [ query , setQuery ] = useState ( '' ) ;
4671
47- const handleSelectOrganization = React . useCallback (
72+ const handleSelectOrganization = useCallback (
4873 ( organizationId : string | null ) => {
4974 if ( organizationId === activeOrganization ?. id ) {
5075 return ;
@@ -58,21 +83,23 @@ export function OrganizationSelector() {
5883 [ activeOrganization , setActiveOrganization ]
5984 ) ;
6085
61- const handleCreateOrganization = React . useCallback ( ( ) => {
86+ const handleCreateOrganization = useCallback ( ( ) => {
6287 setShowCreateDialog ( true ) ;
6388 setIsOpen ( false ) ;
6489 } , [ ] ) ;
6590
66- const handleManageOrganizations = React . useCallback ( ( ) => {
91+ const handleManageOrganizations = useCallback ( ( ) => {
6792 router . push ( '/organizations' ) ;
6893 setIsOpen ( false ) ;
6994 } , [ router ] ) ;
7095
96+ const filteredOrganizations = filterOrganizations ( organizations , query ) ;
97+
7198 if ( isLoading ) {
7299 return (
73100 < div className = "rounded border border-border/50 bg-accent/30 px-2 py-2" >
74101 < div className = "flex items-center gap-3" >
75- < Skeleton className = "h-8 w-8 rounded-full " />
102+ < Skeleton className = "h-8 w-8 rounded" />
76103 < div className = "space-y-1" >
77104 < Skeleton className = "h-4 w-24 rounded" />
78105 < Skeleton className = "h-3 w-16 rounded" />
@@ -82,19 +109,40 @@ export function OrganizationSelector() {
82109 ) ;
83110 }
84111
112+ if ( hasError ) {
113+ return (
114+ < div className = "rounded border border-destructive/50 bg-destructive/10 px-3 py-2 text-destructive" >
115+ < div className = "flex items-center gap-2 text-sm" >
116+ < span > Failed to load workspaces</ span >
117+ </ div >
118+ </ div >
119+ ) ;
120+ }
121+
85122 return (
86123 < >
87- < DropdownMenu onOpenChange = { setIsOpen } open = { isOpen } >
124+ < DropdownMenu
125+ onOpenChange = { ( open ) => {
126+ setIsOpen ( open ) ;
127+ if ( ! open ) {
128+ setQuery ( '' ) ;
129+ }
130+ } }
131+ open = { isOpen }
132+ >
88133 < DropdownMenuTrigger asChild >
89134 < Button
90- className = "h-auto w-full p-0 hover:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0"
135+ aria-expanded = { isOpen }
136+ aria-haspopup = "listbox"
137+ className = "h-auto w-full p-0 hover:bg-transparent"
91138 disabled = { isSettingActiveOrganization }
139+ type = "button"
92140 variant = "ghost"
93141 >
94142 < div
95143 className = { cn (
96- 'w-full rounded border border-border/50 bg-accent/30 px-2 py-2 transition-all duration-200 ' ,
97- 'hover:border-border/70 hover:bg-accent/50' ,
144+ 'w-full rounded border border-border/50 bg-accent/30 px-2 py-2 transition-colors ' ,
145+ 'hover:border-border/70 hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 ' ,
98146 isSettingActiveOrganization && 'cursor-not-allowed opacity-70' ,
99147 isOpen && 'border-border/70 bg-accent/50'
100148 ) }
@@ -111,35 +159,41 @@ export function OrganizationSelector() {
111159 getOrganizationInitials ( activeOrganization . name )
112160 ) : (
113161 < UserIcon
114- className = "h-4 w-4"
115- size = { 32 }
162+ className = "not-dark:text-primary"
116163 weight = "duotone"
117164 />
118165 ) }
119166 </ AvatarFallback >
120167 </ Avatar >
121168 < div className = "flex min-w-0 flex-col text-left" >
122- < span className = "max-w-[140px] truncate font-medium text-sm" >
169+ < span className = "max-w-[140px] truncate font-medium text-sm sm:max-w-[180px] " >
123170 { activeOrganization ?. name || 'Personal' }
124171 </ span >
125- < span className = "max-w-[140px] truncate text-muted-foreground text-xs" >
172+ < span className = "max-w-[140px] truncate text-muted-foreground text-xs sm:max-w-[180px] " >
126173 { activeOrganization ?. slug || 'Your workspace' }
127174 </ span >
128175 </ div >
129176 </ div >
130- < CaretDownIcon
131- className = { cn (
132- 'h-4 w-4 text-muted-foreground transition-transform duration-200' ,
133- isOpen && 'rotate-180'
134- ) }
135- size = { 32 }
136- weight = "duotone"
137- />
177+ { isSettingActiveOrganization ? (
178+ < SpinnerGapIcon
179+ aria-label = "Switching workspace"
180+ className = "h-4 w-4 animate-spin text-muted-foreground"
181+ weight = "duotone"
182+ />
183+ ) : (
184+ < CaretDownIcon
185+ className = { cn (
186+ 'h-4 w-4 text-muted-foreground transition-transform duration-200' ,
187+ isOpen && 'rotate-180'
188+ ) }
189+ weight = "fill"
190+ />
191+ ) }
138192 </ div >
139193 </ div >
140194 </ Button >
141195 </ DropdownMenuTrigger >
142- < DropdownMenuContent align = "start" className = "w-64 p-1" sideOffset = { 4 } >
196+ < DropdownMenuContent align = "start" className = "w-72 p-1" sideOffset = { 4 } >
143197 { /* Personal Workspace */ }
144198 < DropdownMenuItem
145199 className = { cn (
@@ -151,7 +205,7 @@ export function OrganizationSelector() {
151205 >
152206 < Avatar className = "h-6 w-6" >
153207 < AvatarFallback className = "bg-muted text-xs" >
154- < UserIcon className = "h-4 w-4" size = { 32 } weight = "duotone" />
208+ < UserIcon className = "not-dark:text-primary" weight = "duotone" />
155209 </ AvatarFallback >
156210 </ Avatar >
157211 < div className = "flex min-w-0 flex-1 flex-col" >
@@ -162,17 +216,16 @@ export function OrganizationSelector() {
162216 </ div >
163217 { ! activeOrganization && (
164218 < CheckIcon
165- className = "h-4 w-4 text-primary"
166- size = { 32 }
219+ className = "h-4 w-4 not-dark:text-primary"
167220 weight = "duotone"
168221 />
169222 ) }
170223 </ DropdownMenuItem >
171224
172- { organizations && organizations . length > 0 && (
173- < >
225+ { filteredOrganizations && filteredOrganizations . length > 0 && (
226+ < div className = "flex flex-col gap-1" >
174227 < DropdownMenuSeparator className = "my-1" />
175- { organizations . map ( ( org ) => (
228+ { filteredOrganizations . map ( ( org ) => (
176229 < DropdownMenuItem
177230 className = { cn (
178231 'flex cursor-pointer items-center gap-3 rounded px-2 py-2 transition-colors' ,
@@ -200,35 +253,36 @@ export function OrganizationSelector() {
200253 { activeOrganization ?. id === org . id && (
201254 < CheckIcon
202255 className = "h-4 w-4 text-primary"
203- size = { 32 }
204256 weight = "duotone"
205257 />
206258 ) }
207259 </ DropdownMenuItem >
208260 ) ) }
209- </ >
261+ </ div >
262+ ) }
263+
264+ { filteredOrganizations . length === 0 && (
265+ < div className = "px-2 py-2 text-muted-foreground text-xs" >
266+ No workspaces match “{ query } ”.
267+ </ div >
210268 ) }
211269
212270 < DropdownMenuSeparator className = "my-1" />
213271 < DropdownMenuItem
214272 className = "flex cursor-pointer items-center gap-3 rounded px-2 py-2 transition-colors focus:bg-accent focus:text-accent-foreground"
215273 onClick = { handleCreateOrganization }
216274 >
217- < div className = "flex h-6 w-6 items-center justify-center rounded-full bg-muted" >
218- < PlusIcon className = "h-4 w-4 text-muted-foreground" size = { 32 } />
275+ < div className = "flex h-6 w-6 items-center justify-center rounded bg-muted" >
276+ < PlusIcon className = "not-dark: text-primary" />
219277 </ div >
220278 < span className = "font-medium text-sm" > Create Organization</ span >
221279 </ DropdownMenuItem >
222280 < DropdownMenuItem
223281 className = "flex cursor-pointer items-center gap-3 rounded px-2 py-2 transition-colors focus:bg-accent focus:text-accent-foreground"
224282 onClick = { handleManageOrganizations }
225283 >
226- < div className = "flex h-6 w-6 items-center justify-center rounded-full bg-muted" >
227- < UsersIcon
228- className = "h-4 w-4 text-muted-foreground"
229- size = { 32 }
230- weight = "duotone"
231- />
284+ < div className = "flex h-6 w-6 items-center justify-center rounded bg-muted" >
285+ < UsersIcon className = "not-dark:text-primary" weight = "duotone" />
232286 </ div >
233287 < span className = "font-medium text-sm" > Manage Organizations</ span >
234288 </ DropdownMenuItem >
0 commit comments