@@ -13,11 +13,12 @@ import {
1313 CommandSeparator ,
1414} from '@comp/ui/command' ;
1515import { Dialog , DialogContent , DialogTitle , DialogTrigger } from '@comp/ui/dialog' ;
16+ import { DropdownMenu , DropdownMenuContent , DropdownMenuTrigger } from '@comp/ui/dropdown-menu' ;
1617import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '@comp/ui/select' ;
1718import { useSidebar } from '@comp/ui/sidebar' ;
1819import type { Organization } from '@db' ;
1920import { Check , ChevronsUpDown , Loader2 , Plus , Search } from 'lucide-react' ;
20- import { useAction } from 'next-safe-action/hooks' ;
21+ import { useAction , type HookActionStatus } from 'next-safe-action/hooks' ;
2122import Image from 'next/image' ;
2223import { useRouter } from 'next/navigation' ;
2324import { useQueryState } from 'nuqs' ;
@@ -63,7 +64,6 @@ function OrganizationAvatar({
6364} : OrganizationAvatarProps ) {
6465 const sizeClass = size === 'sm' ? 'h-6 w-6' : 'h-8 w-8' ;
6566
66- // If logo URL exists, show the image
6767 if ( logoUrl ) {
6868 return (
6969 < div className = { cn ( 'relative overflow-hidden rounded-sm border' , sizeClass , className ) } >
@@ -72,7 +72,6 @@ function OrganizationAvatar({
7272 ) ;
7373 }
7474
75- // Fallback to initials
7675 const initials = name ?. slice ( 0 , 2 ) . toUpperCase ( ) || '' ;
7776
7877 let colorIndex = 0 ;
@@ -98,6 +97,99 @@ function OrganizationAvatar({
9897 ) ;
9998}
10099
100+ function OrganizationSwitcherContent ( {
101+ sortedOrganizations,
102+ currentOrganization,
103+ logoUrls,
104+ sortOrder,
105+ setSortOrder,
106+ status,
107+ pendingOrgId,
108+ handleOrgChange,
109+ handleOpenChange,
110+ router,
111+ getDisplayName,
112+ } : {
113+ sortedOrganizations : Organization [ ] ;
114+ currentOrganization : Organization | null ;
115+ logoUrls : Record < string , string > ;
116+ sortOrder : string ;
117+ setSortOrder : ( value : string ) => void ;
118+ status : HookActionStatus ;
119+ pendingOrgId : string | null ;
120+ handleOrgChange : ( org : Organization ) => void ;
121+ handleOpenChange : ( open : boolean ) => void ;
122+ router : ReturnType < typeof useRouter > ;
123+ getDisplayName : ( org : Organization ) => string ;
124+ } ) {
125+ return (
126+ < Command >
127+ < div className = "flex items-center border-b px-3" >
128+ < Search className = "mr-2 h-4 w-4 shrink-0 opacity-50" />
129+ < CommandInput
130+ placeholder = "Search organization..."
131+ className = "placeholder:text-muted-foreground flex h-11 w-full rounded-md border-0 bg-transparent py-3 text-sm outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50"
132+ />
133+ </ div >
134+ < div className = "p-2" >
135+ < Select value = { sortOrder } onValueChange = { setSortOrder } >
136+ < SelectTrigger className = "w-full" >
137+ < SelectValue placeholder = "Sort by..." />
138+ </ SelectTrigger >
139+ < SelectContent >
140+ < SelectItem value = "alphabetical" > Alphabetical</ SelectItem >
141+ < SelectItem value = "recent" > Recently Created</ SelectItem >
142+ </ SelectContent >
143+ </ Select >
144+ </ div >
145+ < CommandList >
146+ < CommandEmpty > No results found</ CommandEmpty >
147+ < CommandGroup className = "max-h-[300px] overflow-y-auto" >
148+ { sortedOrganizations . map ( ( org ) => (
149+ < CommandItem
150+ key = { org . id }
151+ value = { `${ org . id } ${ org . name || '' } ` }
152+ onSelect = { ( ) => {
153+ if ( org . id !== currentOrganization ?. id ) {
154+ handleOrgChange ( org ) ;
155+ } else {
156+ handleOpenChange ( false ) ;
157+ }
158+ } }
159+ disabled = { status === 'executing' }
160+ className = "flex items-center gap-2"
161+ >
162+ { status === 'executing' && pendingOrgId === org . id ? (
163+ < Loader2 className = "h-4 w-4 animate-spin" />
164+ ) : currentOrganization ?. id === org . id ? (
165+ < Check className = "h-4 w-4" />
166+ ) : (
167+ < div className = "h-4 w-4" />
168+ ) }
169+ < OrganizationAvatar name = { org . name } logoUrl = { logoUrls [ org . id ] } size = "sm" />
170+ < span className = "truncate" > { getDisplayName ( org ) } </ span >
171+ </ CommandItem >
172+ ) ) }
173+ </ CommandGroup >
174+ < CommandSeparator />
175+ < CommandGroup >
176+ < CommandItem
177+ onSelect = { ( ) => {
178+ router . push ( '/setup?intent=create-additional' ) ;
179+ handleOpenChange ( false ) ;
180+ } }
181+ disabled = { status === 'executing' }
182+ className = "flex items-center gap-2"
183+ >
184+ < Plus className = "h-4 w-4" />
185+ Create Organization
186+ </ CommandItem >
187+ </ CommandGroup >
188+ </ CommandList >
189+ </ Command >
190+ ) ;
191+ }
192+
101193export function OrganizationSwitcher ( {
102194 organizations,
103195 organization,
@@ -106,7 +198,7 @@ export function OrganizationSwitcher({
106198 const { state } = useSidebar ( ) ;
107199 const isCollapsed = state === 'collapsed' ;
108200 const router = useRouter ( ) ;
109- const [ isDialogOpen , setIsDialogOpen ] = useState ( false ) ;
201+ const [ isOpen , setIsOpen ] = useState ( false ) ;
110202 const [ pendingOrgId , setPendingOrgId ] = useState < string | null > ( null ) ;
111203 const [ sortOrder , setSortOrder ] = useState ( 'alphabetical' ) ;
112204
@@ -145,7 +237,8 @@ export function OrganizationSwitcher({
145237 if ( orgId ) {
146238 router . push ( `/${ orgId } /` ) ;
147239 }
148- setIsDialogOpen ( false ) ;
240+ setIsOpen ( false ) ;
241+ setShowOrganizationSwitcher ( null ) ;
149242 setPendingOrgId ( null ) ;
150243 } ,
151244 onExecute : ( args ) => {
@@ -181,105 +274,68 @@ export function OrganizationSwitcher({
181274 } ;
182275
183276 const handleOpenChange = ( open : boolean ) => {
184- setShowOrganizationSwitcher ( open ) ;
185- setIsDialogOpen ( open ) ;
277+ setShowOrganizationSwitcher ( open ? true : null ) ;
278+ setIsOpen ( open ) ;
186279 } ;
187280
188- return (
189- < div className = "w-full" >
190- < Dialog open = { showOrganizationSwitcher ?? isDialogOpen } onOpenChange = { handleOpenChange } >
191- < DialogTrigger asChild >
192- < Button
193- variant = "ghost"
194- size = { isCollapsed ? 'icon' : 'default' }
195- className = { cn (
196- isCollapsed ? 'h-10 w-10 justify-center p-0' : 'h-10 w-full justify-start p-1 pr-2' ,
197- status === 'executing' && 'cursor-not-allowed opacity-50' ,
198- ) }
199- disabled = { status === 'executing' }
200- >
201- < OrganizationAvatar
202- name = { currentOrganization ?. name }
203- logoUrl = { currentOrganization ?. id ? logoUrls [ currentOrganization . id ] : undefined }
204- className = "shrink-0"
205- />
206- { ! isCollapsed && (
207- < >
208- < span className = "ml-2 flex-1 truncate text-left" > { currentOrganization ?. name } </ span >
209- < ChevronsUpDown className = "ml-auto h-4 w-4 shrink-0 opacity-50" />
210- </ >
211- ) }
212- </ Button >
213- </ DialogTrigger >
281+ const isOpenState = showOrganizationSwitcher ?? isOpen ;
282+
283+ const triggerButton = (
284+ < Button
285+ variant = "ghost"
286+ size = { isCollapsed ? 'icon' : 'default' }
287+ className = { cn (
288+ isCollapsed ? 'size-10 p-0' : 'h-10 w-full justify-start p-1 pr-2' ,
289+ status === 'executing' && 'cursor-not-allowed opacity-50' ,
290+ ) }
291+ disabled = { status === 'executing' }
292+ >
293+ < OrganizationAvatar
294+ name = { currentOrganization ?. name }
295+ logoUrl = { currentOrganization ?. id ? logoUrls [ currentOrganization . id ] : undefined }
296+ className = "shrink-0"
297+ />
298+ { ! isCollapsed && (
299+ < >
300+ < span className = "ml-2 flex-1 truncate text-left" > { currentOrganization ?. name } </ span >
301+ < ChevronsUpDown className = "ml-auto h-4 w-4 shrink-0 opacity-50" />
302+ </ >
303+ ) }
304+ </ Button >
305+ ) ;
306+
307+ const contentProps = {
308+ sortedOrganizations,
309+ currentOrganization,
310+ logoUrls,
311+ sortOrder,
312+ setSortOrder,
313+ status,
314+ pendingOrgId,
315+ handleOrgChange,
316+ handleOpenChange,
317+ router,
318+ getDisplayName,
319+ } ;
320+
321+ if ( isCollapsed ) {
322+ return (
323+ < Dialog open = { isOpenState } onOpenChange = { handleOpenChange } >
324+ < DialogTrigger asChild > { triggerButton } </ DialogTrigger >
214325 < DialogContent className = "p-0 sm:max-w-[400px]" >
215326 < DialogTitle className = "sr-only" > Select Organization</ DialogTitle >
216- < Command >
217- < div className = "flex items-center border-b px-3" >
218- < Search className = "mr-2 h-4 w-4 shrink-0 opacity-50" />
219- < CommandInput
220- placeholder = "Search organization..."
221- className = "placeholder:text-muted-foreground flex h-11 w-full rounded-md border-0 bg-transparent py-3 text-sm outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50"
222- />
223- </ div >
224- < div className = "p-2" >
225- < Select value = { sortOrder } onValueChange = { setSortOrder } >
226- < SelectTrigger className = "w-full" >
227- < SelectValue placeholder = "Sort by..." />
228- </ SelectTrigger >
229- < SelectContent >
230- < SelectItem value = "alphabetical" > Alphabetical</ SelectItem >
231- < SelectItem value = "recent" > Recently Created</ SelectItem >
232- </ SelectContent >
233- </ Select >
234- </ div >
235- < CommandList >
236- < CommandEmpty > No results found</ CommandEmpty >
237- < CommandGroup className = "max-h-[300px] overflow-y-auto" >
238- { sortedOrganizations . map ( ( org ) => (
239- < CommandItem
240- key = { org . id }
241- // Search by id and name
242- value = { `${ org . id } ${ org . name || '' } ` }
243- onSelect = { ( ) => {
244- if ( org . id !== currentOrganization ?. id ) {
245- handleOrgChange ( org ) ;
246- } else {
247- handleOpenChange ( false ) ;
248- }
249- } }
250- disabled = { status === 'executing' }
251- className = "flex items-center gap-2"
252- >
253- { status === 'executing' && pendingOrgId === org . id ? (
254- < Loader2 className = "h-4 w-4 animate-spin" />
255- ) : currentOrganization ?. id === org . id ? (
256- < Check className = "h-4 w-4" />
257- ) : (
258- < div className = "h-4 w-4" />
259- ) }
260- < OrganizationAvatar name = { org . name } logoUrl = { logoUrls [ org . id ] } size = "sm" />
261- < span className = "truncate" > { getDisplayName ( org ) } </ span >
262- </ CommandItem >
263- ) ) }
264- </ CommandGroup >
265- < CommandSeparator />
266- < CommandGroup >
267- < CommandItem
268- onSelect = { ( ) => {
269- router . push ( '/setup?intent=create-additional' ) ;
270- setIsDialogOpen ( false ) ;
271- } }
272- disabled = { status === 'executing' }
273- className = "flex items-center gap-2"
274- >
275- < Plus className = "h-4 w-4" />
276- Create Organization
277- </ CommandItem >
278- </ CommandGroup >
279- </ CommandList >
280- </ Command >
327+ < OrganizationSwitcherContent { ...contentProps } />
281328 </ DialogContent >
282329 </ Dialog >
283- </ div >
330+ ) ;
331+ }
332+
333+ return (
334+ < DropdownMenu open = { isOpenState } onOpenChange = { handleOpenChange } >
335+ < DropdownMenuTrigger asChild > { triggerButton } </ DropdownMenuTrigger >
336+ < DropdownMenuContent align = "start" className = "w-[300px] p-0" >
337+ < OrganizationSwitcherContent { ...contentProps } />
338+ </ DropdownMenuContent >
339+ </ DropdownMenu >
284340 ) ;
285341}
0 commit comments