CommandMenuItem.tsx122 lines · main
1'use client'
2
3import { forwardRef, type PropsWithChildren } from 'react'
4import { cn, CommandItem } from 'ui'
5
6import { useQuery } from '../api/hooks/queryHooks'
7import { useCommandMenuTelemetryContext } from '../api/hooks/useCommandMenuTelemetryContext'
8import { useCrossCompatRouter } from '../api/hooks/useCrossCompatRouter'
9import { useResetCommandMenu, useSetCommandMenuOpen } from '../api/hooks/viewHooks'
10import type { IActionCommand, ICommand, IRouteCommand } from './types'
11
12const isActionCommand = (command: ICommand): command is IActionCommand => 'action' in command
13const isRouteCommand = (command: ICommand): command is IRouteCommand => 'route' in command
14
15const generateCommandClassNames = (isLink: boolean) =>
16 cn(
17 'cursor-default',
18 'select-none',
19 'items-center gap-2',
20 'rounded-md',
21 'text-sm',
22 'group',
23 'py-2 md:py-3',
24 'text-foreground-light',
25 'relative',
26 'flex',
27 isLink
28 ? `
29bg-transparent
30px-2
31transition-all
32outline-hidden
33aria-selected:border-overlay
34aria-selected:bg-selection/90
35aria-selected:shadow-xs
36aria-selected:scale-[100.3%]
37data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50`
38 : `
39px-2
40aria-selected:bg-selection/80
41aria-selected:backdrop-filter
42aria-selected:backdrop-blur-md
43data-[disabled=true]:pointer-events-none
44data-[disabled=true]:opacity-50
45`
46 )
47
48interface CommandItemProps extends React.ComponentPropsWithoutRef<typeof CommandItem> {
49 command: ICommand
50}
51
52const CommandMenuItem = forwardRef<
53 React.ElementRef<typeof CommandItem>,
54 PropsWithChildren<CommandItemProps>
55>(({ children, className, command: _command, ...props }, ref) => {
56 const router = useCrossCompatRouter()
57 const setIsOpen = useSetCommandMenuOpen()
58 const resetCommandMenu = useResetCommandMenu()
59 const telemetryContext = useCommandMenuTelemetryContext()
60 const query = useQuery()
61
62 const command = _command as ICommand // strip the readonly applied from the proxy
63
64 const handleCommandSelect = () => {
65 // Send telemetry event
66 if (telemetryContext?.onTelemetry) {
67 const event = {
68 action: 'command_menu_command_clicked' as const,
69 properties: {
70 command_name: command.name,
71 command_value: command.value,
72 command_type: isActionCommand(command) ? ('action' as const) : ('route' as const),
73 search_query: query || undefined,
74 result_path: isRouteCommand(command) ? command.route : undefined,
75 app: telemetryContext.app,
76 },
77 groups: {},
78 }
79
80 telemetryContext.onTelemetry(event)
81 }
82
83 // Execute the original command logic
84 if (isActionCommand(command)) {
85 command.action()
86 } else if (isRouteCommand(command)) {
87 if (command.route.startsWith('http')) {
88 setIsOpen(false)
89 window.open(command.route, '_blank', 'noreferrer,noopener')
90 resetCommandMenu()
91 } else {
92 router.push(command.route)
93 }
94 }
95 }
96
97 return (
98 <CommandItem
99 ref={ref}
100 onSelect={handleCommandSelect}
101 value={command.value ?? command.name}
102 forceMount={command.forceMount}
103 className={cn(
104 generateCommandClassNames(isRouteCommand(command)),
105 className,
106 command.className
107 )}
108 {...props}
109 >
110 <div className="w-full flex flex-row justify-between items-center">
111 <div className="flex flex-row gap-2 grow items-center">
112 {command.icon?.()}
113 {children}
114 </div>
115 {command.badge?.()}
116 </div>
117 </CommandItem>
118 )
119})
120CommandMenuItem.displayName = 'CommandMenuItem'
121
122export { CommandMenuItem, generateCommandClassNames }