mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 07:39:07 +03:00
feat: finish command menu base
This commit is contained in:
1 parent
ce808954df
commit
649257b4ef
23 files changed
+612
-23
No files matched your search
Generated
+2
@@ -42943,6 +42943,7 @@
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "*",
|
||||
"react-use": "^17.5.0",
|
||||
"reactflow": "*",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sql-formatter": "^15.3.1",
|
||||
@@ -42962,6 +42963,7 @@
|
||||
"api-types": "*",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"next-router-mock": "^0.9.13",
|
||||
"ts-jest": "^29.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Microscope } from 'lucide-react'
|
||||
|
||||
import { Badge } from 'ui'
|
||||
|
||||
interface BadgeExperimentalProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function BadgeExperimental({ className }: BadgeExperimentalProps) {
|
||||
return (
|
||||
<Badge className={className}>
|
||||
<Microscope className="!mr-1.5 !w-3.5 !h-3.5" /> Experimental
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
export { BadgeExperimental }
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { CommandInput_Shadcn_, cn } from 'ui'
|
||||
|
||||
import { useQuery, useSetQuery } from './hooks/queryHooks'
|
||||
|
||||
const CommandInput = forwardRef<
|
||||
React.ElementRef<typeof CommandInput_Shadcn_>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandInput_Shadcn_>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const query = useQuery()
|
||||
const setQuery = useSetQuery()
|
||||
|
||||
return (
|
||||
<CommandInput_Shadcn_
|
||||
autoFocus
|
||||
ref={ref}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
placeholder="Type a command or search..."
|
||||
className={cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent px-4 py-7 text-sm outline-none',
|
||||
'focus:shadow-none focus:ring-transparent',
|
||||
'text-foreground-light placeholder:text-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 border-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
CommandInput.displayName = CommandInput_Shadcn_.displayName
|
||||
|
||||
export { CommandInput }
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, useRef } from 'react'
|
||||
|
||||
import { CommandList_Shadcn_, cn } from 'ui'
|
||||
|
||||
import { CommandItem, type ICommand } from '../internal/Command'
|
||||
import { CommandEmpty } from '../internal/CommandEmpty'
|
||||
import { CommandGroup } from '../internal/CommandGroup'
|
||||
import { useCommands } from './hooks/commandsHooks'
|
||||
import { useQuery } from './hooks/queryHooks'
|
||||
import { TextHighlighter } from './TextHighlighter'
|
||||
|
||||
const CommandList = forwardRef<
|
||||
React.ElementRef<typeof CommandList_Shadcn_>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandList_Shadcn_>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const commandSections = useCommands()
|
||||
const query = useQuery()
|
||||
|
||||
const innerRef = useRef<HTMLDivElement | undefined>(undefined)
|
||||
const setInnerRef = (elem: HTMLDivElement) => (innerRef.current = elem)
|
||||
|
||||
const setRef = (elem: HTMLDivElement) => {
|
||||
if (ref) typeof ref === 'function' ? ref(elem) : (ref.current = elem)
|
||||
setInnerRef(elem)
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandList_Shadcn_
|
||||
ref={setRef}
|
||||
className={cn('overflow-y-auto overflow-x-hidden bg-transparent', className)}
|
||||
{...props}
|
||||
>
|
||||
<CommandEmpty listRef={innerRef}>No results found.</CommandEmpty>
|
||||
{commandSections.map((section) => {
|
||||
if (section.commands.every((command) => command.defaultHidden) && !query) return null
|
||||
|
||||
return (
|
||||
<CommandGroup key={section.id} heading={section.name} forceMount={section.forceMount}>
|
||||
{section.commands
|
||||
.filter((command) => !command.defaultHidden || query)
|
||||
.map((_command) => {
|
||||
const command = _command as ICommand // strip the readonly applied from the proxy
|
||||
|
||||
return (
|
||||
<CommandItem key={command.id} command={command}>
|
||||
<TextHighlighter>{command.name}</TextHighlighter>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
)
|
||||
})}
|
||||
</CommandList_Shadcn_>
|
||||
)
|
||||
})
|
||||
CommandList.displayName = CommandList_Shadcn_.displayName
|
||||
|
||||
export { CommandList }
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
import { AlertTriangle, ArrowLeft } from 'lucide-react'
|
||||
import { type PropsWithChildren, forwardRef, useEffect, useState } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import { Button, Command_Shadcn_, Dialog, DialogContent, cn } from 'ui'
|
||||
|
||||
import { useCurrentPage, usePageComponent, usePopPage } from './hooks/pagesHooks'
|
||||
import { useQuery, useSetQuery } from './hooks/queryHooks'
|
||||
import { useCommandMenuSize, useCommandMenuOpen, useSetCommandMenuOpen } from './hooks/viewHooks'
|
||||
import { PageType } from './utils'
|
||||
|
||||
function Breadcrumb() {
|
||||
const currPage = useCurrentPage()
|
||||
const popPage = usePopPage()
|
||||
|
||||
if (!currPage) return
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 mt-2 ml-2 text-xs text-foreground-muted">
|
||||
<button onClick={popPage}>
|
||||
<ArrowLeft width={12} height={12} />
|
||||
</button>
|
||||
{currPage.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandWrapper = forwardRef<
|
||||
React.ElementRef<typeof Command_Shadcn_>,
|
||||
React.ComponentPropsWithoutRef<typeof Command_Shadcn_>
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
const currPage = useCurrentPage()
|
||||
|
||||
return (
|
||||
<Command_Shadcn_
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-full w-full flex flex-col overflow-hidden',
|
||||
'[&_[cmdk-group]]:px-2 [&_[cmdk-group]]:!bg-transparent [&_[cmdk-group-heading]]:!bg-transparent [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-border-stronger [&_[cmdk-input]]:h-12',
|
||||
'[&_[cmdk-item]_svg]:h-5',
|
||||
'[&_[cmdk-item]_svg]:w-5',
|
||||
'[&_[cmdk-item]_svg]:stroke-1',
|
||||
'[&_[cmdk-input-wrapper]_svg]:h-5',
|
||||
'[&_[cmdk-input-wrapper]_svg]:w-5',
|
||||
|
||||
'[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{currPage?.type === PageType.Commands && <Breadcrumb />}
|
||||
{children}
|
||||
</Command_Shadcn_>
|
||||
)
|
||||
})
|
||||
CommandWrapper.displayName = Command_Shadcn_.displayName
|
||||
|
||||
function CommandError({ resetErrorBoundary }: { resetErrorBoundary: () => void }) {
|
||||
return (
|
||||
<div className={cn('min-h-64', 'flex items-center justify-center')}>
|
||||
<div className="p-10 flex flex-col items-center gap-6 mt-4">
|
||||
<AlertTriangle strokeWidth={1.5} size={40} />
|
||||
<p className="text-lg text-center">
|
||||
Sorry, looks like we're having some issues with the command menu!
|
||||
</p>
|
||||
<p className="text-sm text-center">Please try again in a bit.</p>
|
||||
<Button size="tiny" type="secondary" onClick={resetErrorBoundary}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PageSwitch({ children }: PropsWithChildren) {
|
||||
const PageComponent = usePageComponent()
|
||||
|
||||
return PageComponent ? <PageComponent /> : <CommandWrapper>{children}</CommandWrapper>
|
||||
}
|
||||
|
||||
function CommandMenu({ children }: PropsWithChildren) {
|
||||
const open = useCommandMenuOpen()
|
||||
const setOpen = useSetCommandMenuOpen()
|
||||
|
||||
const size = useCommandMenuSize()
|
||||
|
||||
const page = useCurrentPage()
|
||||
const popPage = usePopPage()
|
||||
|
||||
const query = useQuery()
|
||||
const setQuery = useSetQuery()
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
hideClose
|
||||
onInteractOutside={() => setOpen(false)}
|
||||
onEscapeKeyDown={(e) => {
|
||||
e.preventDefault()
|
||||
return query ? setQuery('') : page ? popPage() : setOpen(false)
|
||||
}}
|
||||
size={size}
|
||||
className={cn('place-self-start mx-auto top-24')}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={CommandError}>
|
||||
<PageSwitch>{children}</PageSwitch>
|
||||
</ErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export { CommandMenu, CommandWrapper }
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useConstant } from 'common'
|
||||
import { useEffect, type PropsWithChildren } from 'react'
|
||||
import { type PropsWithChildren, useEffect, useMemo, useCallback } from 'react'
|
||||
|
||||
import { useToggleCommandMenu } from './hooks/viewHooks'
|
||||
import { CommandContext } from '../internal/Context'
|
||||
import { initCommandsState } from '../internal/state/commandsState'
|
||||
import { initPagesState } from '../internal/state/pagesState'
|
||||
import { initQueryState } from '../internal/state/queryState'
|
||||
import { initViewState } from '../internal/state/viewState'
|
||||
import { useCrossCompatRouter } from './hooks/useCrossCompatRouter'
|
||||
import { useSetCommandMenuOpen, useToggleCommandMenu } from './hooks/viewHooks'
|
||||
|
||||
const CommandProviderInternal = ({ children }: PropsWithChildren) => {
|
||||
const combinedState = useConstant(() => ({
|
||||
@@ -19,6 +22,7 @@ const CommandProviderInternal = ({ children }: PropsWithChildren) => {
|
||||
return <CommandContext.Provider value={combinedState}>{children}</CommandContext.Provider>
|
||||
}
|
||||
|
||||
// This is a component not a hook so it can access the wrapping context.
|
||||
const CommandShortcut = () => {
|
||||
const toggleOpen = useToggleCommandMenu()
|
||||
|
||||
@@ -38,9 +42,27 @@ const CommandShortcut = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
// This is a component not a hook so it can access the wrapping context.
|
||||
function CloseOnNavigation() {
|
||||
const setIsOpen = useSetCommandMenuOpen()
|
||||
const router = useCrossCompatRouter()
|
||||
|
||||
const completeNavigation = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
}, [setIsOpen])
|
||||
|
||||
useEffect(() => {
|
||||
router.events.onRouteChangeComplete(completeNavigation)
|
||||
return () => router.events.offRouteChangeComplete(completeNavigation)
|
||||
}, [router])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const CommandProvider = ({ children }: PropsWithChildren) => (
|
||||
<CommandProviderInternal>
|
||||
<CommandShortcut />
|
||||
<CloseOnNavigation />
|
||||
{children}
|
||||
</CommandProviderInternal>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Children,
|
||||
type DetailedHTMLProps,
|
||||
type HTMLAttributes,
|
||||
type PropsWithChildren,
|
||||
} from 'react'
|
||||
|
||||
import { useQuery } from './hooks/queryHooks'
|
||||
|
||||
interface TextHighlighterBaseProps
|
||||
extends PropsWithChildren<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>> {
|
||||
query: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
function TextHighlighterBase({ children, text, query, ...props }: TextHighlighterBaseProps) {
|
||||
const child = text ?? Children.toArray(children)[0]
|
||||
if (typeof child !== 'string') return child
|
||||
|
||||
const idx = child.toLowerCase().indexOf(query.toLowerCase())
|
||||
if (idx === -1) {
|
||||
return <span {...props}>{child}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...props}>
|
||||
{child.substring(0, idx)}
|
||||
<span className="text-foreground">{child.substring(idx, idx + query.length)}</span>
|
||||
{child.substring(idx + query.length)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function TextHighlighter(props: Omit<TextHighlighterBaseProps, 'query'>) {
|
||||
const query = useQuery()
|
||||
|
||||
return <TextHighlighterBase query={query} {...props} />
|
||||
}
|
||||
|
||||
export { TextHighlighter, TextHighlighterBase }
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { isEqual } from 'lodash'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { isEqual } from 'lodash'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter as useLegacyRouter } from 'next/compat/router'
|
||||
import { useRouter as useNextRouter } from 'next/navigation'
|
||||
import { useEffect, useMemo, useRef, useTransition } from 'react'
|
||||
import { usePrevious } from 'react-use'
|
||||
|
||||
type Handler = (...evts: any[]) => void
|
||||
|
||||
function useHasChanged<T>(value: T, { onlyTo }: { onlyTo?: T } = {}) {
|
||||
const prev = usePrevious(value)
|
||||
|
||||
if (prev === value) return false
|
||||
if (onlyTo && value !== onlyTo) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function useCrossCompatRouter() {
|
||||
const legacyRouter = useLegacyRouter()
|
||||
const newRouter = useNextRouter()
|
||||
const isUsingLegacyRouting = !!legacyRouter
|
||||
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const hasPendingEnded = useHasChanged(isPending, { onlyTo: false })
|
||||
const onPendingEnd = useRef(new Set<() => void>())
|
||||
useEffect(() => {
|
||||
if (!isUsingLegacyRouting && hasPendingEnded) {
|
||||
onPendingEnd.current.forEach((fn) => fn())
|
||||
}
|
||||
}, [hasPendingEnded, isUsingLegacyRouting])
|
||||
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
events: {
|
||||
onRouteChangeComplete: (fn: Handler) => {
|
||||
if (isUsingLegacyRouting) {
|
||||
legacyRouter.events.on('routeChangeComplete', fn)
|
||||
} else {
|
||||
onPendingEnd.current.add(fn)
|
||||
}
|
||||
},
|
||||
offRouteChangeComplete: (fn: Handler) => {
|
||||
if (isUsingLegacyRouting) {
|
||||
legacyRouter.events.off('routeChangeComplete', fn)
|
||||
} else {
|
||||
onPendingEnd.current.delete(fn)
|
||||
}
|
||||
},
|
||||
},
|
||||
push: (path: string) => {
|
||||
if (isUsingLegacyRouting) {
|
||||
legacyRouter.push(path)
|
||||
} else {
|
||||
startTransition(() => {
|
||||
newRouter.push(path)
|
||||
})
|
||||
}
|
||||
},
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
export { useCrossCompatRouter }
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useSetQuery } from './queryHooks'
|
||||
|
||||
interface UseHistoryKeysOptions {
|
||||
stack: string[]
|
||||
enable?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables shell-style message history when hitting up/down on the keyboard
|
||||
*/
|
||||
function useHistoryKeys({ stack, enable = true }: UseHistoryKeysOptions) {
|
||||
const [idx, setIdx] = useState(0)
|
||||
const setQuery = useSetQuery()
|
||||
|
||||
const navigateIdx = useCallback((newIdx: number) => {
|
||||
if (!enable) return
|
||||
|
||||
setQuery(stack[newIdx] ?? '')
|
||||
setIdx(newIdx)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
return navigateIdx(Math.max(idx - 1, 0))
|
||||
case 'ArrowDown':
|
||||
return navigateIdx(Math.min(idx + 1, stack.length - 1))
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [navigateIdx, stack])
|
||||
}
|
||||
|
||||
export { useHistoryKeys }
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
@@ -10,7 +12,7 @@ const useCommandMenuInitiated = () => {
|
||||
return initiated
|
||||
}
|
||||
|
||||
const useCommandMenuVisible = () => {
|
||||
const useCommandMenuOpen = () => {
|
||||
const { viewState } = useCommandContext()
|
||||
const { open } = useSnapshot(viewState)
|
||||
return open
|
||||
@@ -28,18 +30,6 @@ const useToggleCommandMenu = () => {
|
||||
return toggleOpen
|
||||
}
|
||||
|
||||
const useIsCommandNavigating = () => {
|
||||
const { viewState } = useCommandContext()
|
||||
const { isNavigating } = useSnapshot(viewState)
|
||||
return isNavigating
|
||||
}
|
||||
|
||||
const useSetIsCommandNavigating = () => {
|
||||
const { viewState } = useCommandContext()
|
||||
const { setIsNavigating } = useSnapshot(viewState)
|
||||
return setIsNavigating
|
||||
}
|
||||
|
||||
const useCommandMenuSize = () => {
|
||||
const { viewState } = useCommandContext()
|
||||
const { size } = useSnapshot(viewState)
|
||||
@@ -60,11 +50,9 @@ const useSetCommandMenuSize = (newSize: DialogSize) => {
|
||||
|
||||
export {
|
||||
useCommandMenuInitiated,
|
||||
useCommandMenuVisible,
|
||||
useCommandMenuOpen,
|
||||
useSetCommandMenuOpen,
|
||||
useToggleCommandMenu,
|
||||
useIsCommandNavigating,
|
||||
useSetIsCommandNavigating,
|
||||
useCommandMenuSize,
|
||||
useSetCommandMenuSize,
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
const escapeAttributeSelector = (str: string) => encodeURIComponent(str)
|
||||
|
||||
export { escapeAttributeSelector }
|
||||
export { generateCommandClassNames } from '../internal/Command'
|
||||
export { orderSectionFirst } from '../internal/state/commandsState'
|
||||
export { PageType } from '../internal/state/pagesState'
|
||||
@@ -0,0 +1,14 @@
|
||||
export * from './api/Badges'
|
||||
export { CommandInput } from './api/CommandInput'
|
||||
export { CommandList } from './api/CommandList'
|
||||
export { CommandMenu, CommandWrapper } from './api/CommandMenu'
|
||||
export { CommandProvider } from './api/CommandProvider'
|
||||
export { TextHighlighter, TextHighlighterBase } from './api/TextHighlighter'
|
||||
export * from './api/hooks/commandsHooks'
|
||||
export * from './api/hooks/pagesHooks'
|
||||
export * from './api/hooks/queryHooks'
|
||||
export { useHistoryKeys } from './api/hooks/useHistoryKeys'
|
||||
export * from './api/hooks/viewHooks'
|
||||
export * from './api/utils'
|
||||
|
||||
export * from './api/types'
|
||||
@@ -1,4 +1,10 @@
|
||||
import { type ReactNode } from 'react'
|
||||
'use client'
|
||||
|
||||
import { type PropsWithChildren, type ReactNode, forwardRef } from 'react'
|
||||
|
||||
import { CommandItem_Shadcn_, cn } from 'ui'
|
||||
import { useCrossCompatRouter } from '../api/hooks/useCrossCompatRouter'
|
||||
import { useSetCommandMenuOpen } from '../api/hooks/viewHooks'
|
||||
|
||||
type ICommand = IActionCommand | IRouteCommand
|
||||
|
||||
@@ -32,4 +38,89 @@ interface IRouteCommand extends IBaseCommand {
|
||||
route: `/${string}` | `http${string}`
|
||||
}
|
||||
|
||||
const isActionCommand = (command: ICommand): command is IActionCommand => 'action' in command
|
||||
const isRouteCommand = (command: ICommand): command is IRouteCommand => 'route' in command
|
||||
|
||||
const generateCommandClassNames = (isLink: boolean) =>
|
||||
cn(
|
||||
'cursor-default',
|
||||
'select-none',
|
||||
'items-center gap-2',
|
||||
'rounded-md',
|
||||
'text-sm',
|
||||
'group',
|
||||
'py-3',
|
||||
'text-foreground-light',
|
||||
'relative',
|
||||
'flex',
|
||||
isLink
|
||||
? `
|
||||
bg-transparent
|
||||
px-2
|
||||
transition-all
|
||||
outline-none
|
||||
aria-selected:border-overlay
|
||||
aria-selected:bg-selection/90
|
||||
aria-selected:shadow-sm
|
||||
aria-selected:scale-[100.3%]
|
||||
data-[disabled]:pointer-events-none data-[disabled]:opacity-50`
|
||||
: `
|
||||
px-2
|
||||
aria-selected:bg-selection/80
|
||||
aria-selected:backdrop-filter
|
||||
aria-selected:backdrop-blur-md
|
||||
data-[disabled]:pointer-events-none
|
||||
data-[disabled]:opacity-50
|
||||
`
|
||||
)
|
||||
|
||||
interface CommandItemProps extends React.ComponentPropsWithoutRef<typeof CommandItem_Shadcn_> {
|
||||
command: ICommand
|
||||
}
|
||||
|
||||
const CommandItem = forwardRef<
|
||||
React.ElementRef<typeof CommandItem_Shadcn_>,
|
||||
PropsWithChildren<CommandItemProps>
|
||||
>(({ children, className, command: _command, ...props }, ref) => {
|
||||
const router = useCrossCompatRouter()
|
||||
const setIsOpen = useSetCommandMenuOpen()
|
||||
|
||||
const command = _command as ICommand // strip the readonly applied from the proxy
|
||||
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
ref={ref}
|
||||
forceMount={command.forceMount}
|
||||
onSelect={
|
||||
isActionCommand(command)
|
||||
? command.action
|
||||
: isRouteCommand(command)
|
||||
? () => {
|
||||
command.route.startsWith('http')
|
||||
? (setIsOpen(false), window.open(command.route, '_blank', 'noreferrer,noopener'))
|
||||
: router.push(command.route)
|
||||
}
|
||||
: () => {}
|
||||
}
|
||||
value={command.value}
|
||||
className={cn(
|
||||
generateCommandClassNames(isRouteCommand(command)),
|
||||
className,
|
||||
command.className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row gap-2 flex-grow items-center">
|
||||
{command.icon?.()}
|
||||
{children}
|
||||
</div>
|
||||
{command.badge?.()}
|
||||
</div>
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})
|
||||
CommandItem.displayName = CommandItem_Shadcn_.displayName
|
||||
|
||||
export { CommandItem, generateCommandClassNames, isActionCommand, isRouteCommand }
|
||||
export type { ICommand }
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { type PropsWithChildren, type RefObject, useMemo, useState } from 'react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { useQuery } from '../api/hooks/queryHooks'
|
||||
|
||||
const CommandEmpty = ({
|
||||
children,
|
||||
className,
|
||||
listRef,
|
||||
...props
|
||||
}: PropsWithChildren<{
|
||||
className?: string
|
||||
/**
|
||||
* Reference to the div that contains the command item list in the DOM.
|
||||
*
|
||||
* Hacking around a bug in cmdk where the empty state will show even when
|
||||
* there are force-mounted items.
|
||||
*/
|
||||
listRef: RefObject<HTMLDivElement | undefined>
|
||||
}>) => {
|
||||
const query = useQuery()
|
||||
|
||||
const [render, setRender] = useState(false)
|
||||
useMemo(() => {
|
||||
if (!query) setRender(false)
|
||||
/**
|
||||
* In a timeout so the DOM update happens before we query.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
setRender(!listRef?.current?.querySelector('[cmdk-item]'))
|
||||
})
|
||||
}, [query])
|
||||
|
||||
return (
|
||||
render && (
|
||||
<div className={cn('py-6 text-center text-sm text-foreground-muted', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export { CommandEmpty }
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { CommandGroup_Shadcn_, cn } from 'ui'
|
||||
|
||||
const CommandGroup = forwardRef<
|
||||
React.ElementRef<typeof CommandGroup_Shadcn_>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandGroup_Shadcn_>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<CommandGroup_Shadcn_
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden py-3 px-2 text-foreground-muted [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-normal [&_[cmdk-group-heading]]:text-foreground-muted',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
CommandGroup.displayName = CommandGroup_Shadcn_.displayName
|
||||
|
||||
export { CommandGroup }
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
import { type ICommandsState } from '../internal/state/commandsState'
|
||||
|
||||
@@ -8,8 +8,6 @@ type IViewState = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
toggleOpen: () => void
|
||||
isNavigating: boolean
|
||||
setIsNavigating: (isNavigating: boolean) => void
|
||||
size: DialogSize
|
||||
setSize: (size: DialogSize) => void
|
||||
}
|
||||
@@ -27,8 +25,6 @@ const initViewState = () => {
|
||||
state.init()
|
||||
state.open = !state.open
|
||||
},
|
||||
isNavigating: false,
|
||||
setIsNavigating: (isNavigating) => (state.isNavigating = isNavigating),
|
||||
size: 'large',
|
||||
setSize: (size) => (state.size = size),
|
||||
})
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
moduleNameMapper: {
|
||||
'^.+\\.s?css$': '<rootDir>/stubs/cssStub.js',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
jest.mock('next/navigation', () => require('next-router-mock'))
|
||||
@@ -24,6 +24,7 @@
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "*",
|
||||
"react-use": "^17.5.0",
|
||||
"reactflow": "*",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sql-formatter": "^15.3.1",
|
||||
@@ -43,6 +44,7 @@
|
||||
"api-types": "*",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"next-router-mock": "^0.9.13",
|
||||
"ts-jest": "^29.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* A testing stub for CSS imports.
|
||||
*/
|
||||
|
||||
module.exports = {}
|
||||
Reference in new issue
Block a user