Skip to content

Commit d024f53

Browse files
committed
Improve organization dropdown UX and error handling
Increases scroll threshold and page size for organization dropdown, improves organization initials parsing, and adds user-facing error toast on organization switch failure. Moves shimmer keyframes to global CSS for better style management and removes inline style from component.
1 parent 072da62 commit d024f53

File tree

2 files changed

+74
-73
lines changed

2 files changed

+74
-73
lines changed

apps/dashboard/src/components/side-navigation/organization-dropdown-clerk.tsx

Lines changed: 65 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ import {
1111
DropdownMenuItem,
1212
DropdownMenuTrigger,
1313
} from '@/components/primitives/dropdown-menu';
14+
import { showErrorToast } from '@/components/primitives/sonner-helpers';
1415
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/primitives/tooltip';
1516
import { useRegion } from '@/context/region';
1617
import { useFeatureFlag } from '@/hooks/use-feature-flag';
1718
import { ROUTES } from '@/utils/routes';
1819
import { cn } from '@/utils/ui';
1920

20-
const SCROLL_THRESHOLD = 50;
21-
const PAGE_SIZE = 8;
21+
const SCROLL_THRESHOLD = 100;
22+
const PAGE_SIZE = 10;
2223

2324
function getOrganizationInitials(name: string) {
2425
return name
25-
.split(' ')
26+
.trim()
27+
.split(/\s+/)
2628
.map((word) => word[0])
2729
.join('')
2830
.toUpperCase()
@@ -124,6 +126,8 @@ export function OrganizationDropdown() {
124126
setIsOpen(false);
125127
} catch (error) {
126128
console.error('Failed to switch organization:', error);
129+
const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
130+
showErrorToast(`Unable to switch organizations. ${errorMessage}`, 'Organization Switch Failed');
127131
} finally {
128132
setIsSwitching(false);
129133
setSwitchingToId(null);
@@ -173,75 +177,63 @@ export function OrganizationDropdown() {
173177
const filteredMemberships = userMemberships?.data?.filter(filterMemberships) || [];
174178

175179
return (
176-
<>
177-
<style>
178-
{`
179-
@keyframes shimmer {
180-
from { transform: translateX(-100%); }
181-
to { transform: translateX(100%); }
182-
}
183-
`}
184-
</style>
185-
186-
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
187-
<DropdownMenuTrigger asChild>
188-
<button
189-
className={cn(
190-
'group relative flex w-full items-center gap-2 rounded-lg px-1.5 py-1.5 transition-all duration-300',
191-
'hover:bg-background hover:shadow-sm',
192-
'before:absolute before:bottom-0 before:left-0 before:h-0 before:w-full before:border-b before:border-neutral-alpha-100 before:transition-all before:duration-300 before:content-[""]',
193-
'hover:before:border-transparent',
194-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:shadow-sm focus-visible:before:border-transparent'
195-
)}
196-
>
197-
<OrganizationAvatar imageUrl={currentOrganization.imageUrl} name={currentOrganization.name} showShimmer />
198-
<span className="text-sm font-medium text-foreground-950">{currentOrganization.name}</span>
199-
<RiArrowDownSLine className="ml-auto size-4 opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus:opacity-100" />
200-
</button>
201-
</DropdownMenuTrigger>
202-
203-
<DropdownMenuContent className="w-64 p-0" align="start">
204-
<div
205-
ref={scrollContainerRef}
206-
className="max-h-[200px] overflow-y-auto"
207-
role="group"
208-
aria-label="List of all organization memberships"
209-
onScroll={handleScroll}
210-
>
211-
<AnimatePresence mode="popLayout">
212-
{filteredMemberships.map((membership) => (
213-
<OrganizationListItem
214-
key={membership.id}
215-
membership={membership}
216-
onSwitch={handleOrganizationSwitch}
217-
isSwitching={isSwitching}
218-
switchingToId={switchingToId}
219-
/>
220-
))}
221-
</AnimatePresence>
222-
223-
{userMemberships?.isFetching && (
224-
<div className="flex items-center justify-center py-2">
225-
<RiLoader4Line className="size-4 animate-spin text-foreground-600" />
226-
</div>
227-
)}
228-
</div>
229-
230-
<DropdownMenuItem
231-
className={cn(
232-
'flex cursor-pointer items-center gap-2 rounded-none border-t border-neutral-alpha-200 px-3 py-1.5 text-sm transition-shadow focus:bg-accent hover:bg-accent',
233-
isScrolled && 'shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]'
234-
)}
235-
onSelect={() => {
236-
window.location.href = ROUTES.SIGNUP_ORGANIZATION_LIST;
237-
}}
238-
>
239-
<RiAddCircleLine className="size-4" />
240-
<span className="text-foreground-950">Create organization</span>
241-
</DropdownMenuItem>
242-
</DropdownMenuContent>
243-
</DropdownMenu>
244-
</>
180+
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
181+
<DropdownMenuTrigger asChild>
182+
<button
183+
className={cn(
184+
'group relative flex w-full items-center gap-2 rounded-lg px-1.5 py-1.5 transition-all duration-300',
185+
'hover:bg-background hover:shadow-sm',
186+
'before:absolute before:bottom-0 before:left-0 before:h-0 before:w-full before:border-b before:border-neutral-alpha-100 before:transition-all before:duration-300 before:content-[""]',
187+
'hover:before:border-transparent',
188+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:shadow-sm focus-visible:before:border-transparent'
189+
)}
190+
>
191+
<OrganizationAvatar imageUrl={currentOrganization.imageUrl} name={currentOrganization.name} showShimmer />
192+
<span className="text-sm font-medium text-foreground-950">{currentOrganization.name}</span>
193+
<RiArrowDownSLine className="ml-auto size-4 opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus:opacity-100" />
194+
</button>
195+
</DropdownMenuTrigger>
196+
197+
<DropdownMenuContent className="w-64 p-0" align="start">
198+
<div
199+
ref={scrollContainerRef}
200+
className="max-h-[200px] overflow-y-auto"
201+
role="group"
202+
aria-label="List of all organization memberships"
203+
onScroll={handleScroll}
204+
>
205+
<AnimatePresence mode="popLayout">
206+
{filteredMemberships.map((membership) => (
207+
<OrganizationListItem
208+
key={membership.id}
209+
membership={membership}
210+
onSwitch={handleOrganizationSwitch}
211+
isSwitching={isSwitching}
212+
switchingToId={switchingToId}
213+
/>
214+
))}
215+
</AnimatePresence>
216+
217+
{userMemberships?.isFetching && (
218+
<div className="flex items-center justify-center py-2">
219+
<RiLoader4Line className="size-4 animate-spin text-foreground-600" />
220+
</div>
221+
)}
222+
</div>
223+
224+
<DropdownMenuItem
225+
className={cn(
226+
'flex cursor-pointer items-center gap-2 rounded-none border-t border-neutral-alpha-200 px-3 py-1.5 text-sm transition-shadow focus:bg-accent hover:bg-accent',
227+
isScrolled && 'shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]'
228+
)}
229+
onSelect={() => {
230+
window.location.href = ROUTES.SIGNUP_ORGANIZATION_LIST;
231+
}}
232+
>
233+
<RiAddCircleLine className="size-4" />
234+
<span className="text-foreground-950">Create organization</span>
235+
</DropdownMenuItem>
236+
</DropdownMenuContent>
237+
</DropdownMenu>
245238
);
246239
}
247-

apps/dashboard/src/index.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,12 @@
420420
.mly-editor .mly-prose .footer:not(:where([class~="mly-not-prose"], [class~="mly-not-prose"] *)) {
421421
font-size: 14px;
422422
}
423+
424+
@keyframes shimmer {
425+
from {
426+
transform: translateX(-100%);
427+
}
428+
to {
429+
transform: translateX(100%);
430+
}
431+
}

0 commit comments

Comments
 (0)