"use client" import * as React from "react" import { Loader2Icon, MoreHorizontalIcon } from "lucide-react" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuItemDescription, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { cn, stopInteractivePropagation } from "@/lib/utils" export type ActionMenuItem = { key: string label: React.ReactNode section?: React.ReactNode description?: React.ReactNode icon?: React.ReactNode shortcut?: React.ReactNode disabled?: boolean loading?: boolean destructive?: boolean hidden?: boolean keepOpen?: boolean onSelect?: () => void | Promise } export type ActionMenuProps = { actions: ActionMenuItem[] open?: boolean defaultOpen?: boolean onOpenChange?: (open: boolean) => void label?: React.ReactNode triggerLabel?: string trigger?: React.ReactElement align?: "start" | "center" | "end" side?: "top" | "right" | "bottom" | "left" sideOffset?: number disabled?: boolean triggerVariant?: React.ComponentProps["variant"] triggerSize?: React.ComponentProps["size"] showChevron?: boolean closeOnSelect?: boolean contentClassName?: string triggerClassName?: string itemClassName?: string emptyLabel?: React.ReactNode menuWidth?: number | string loadingLabel?: React.ReactNode persistIconSpace?: boolean itemRoleDescription?: string renderItem?: (action: ActionMenuItem, state: { loading: boolean; close: () => void }) => React.ReactNode onActionError?: (error: unknown, action: ActionMenuItem) => void } function ActionMenu({ actions, open: openProp, defaultOpen = false, onOpenChange, label, triggerLabel = "Open actions", trigger, align = "end", side = "bottom", sideOffset = 4, disabled = false, triggerVariant = "ghost", triggerSize = "icon-sm", showChevron = false, closeOnSelect = true, contentClassName, triggerClassName, itemClassName, emptyLabel = "No actions", menuWidth, loadingLabel = "Working...", persistIconSpace = true, itemRoleDescription, renderItem, onActionError, }: ActionMenuProps) { const visibleActions = actions.filter((action) => !action.hidden) const [loadingKey, setLoadingKey] = React.useState(null) const [internalOpen, setInternalOpen] = React.useState(defaultOpen) const open = openProp ?? internalOpen const lastInvokedActionKeyRef = React.useRef(null) const setOpen = React.useCallback((nextOpen: boolean) => { if (openProp === undefined) setInternalOpen(nextOpen) onOpenChange?.(nextOpen) }, [onOpenChange, openProp]) const handleSelect = async (action: ActionMenuItem) => { if (action.disabled || action.loading || loadingKey) return try { setLoadingKey(action.key) await action.onSelect?.() if (closeOnSelect && !action.keepOpen) setOpen(false) } catch (error) { onActionError?.(error, action) if (!onActionError) throw error } finally { setLoadingKey(null) } } const triggerAction = (action: ActionMenuItem) => { if (lastInvokedActionKeyRef.current === action.key) return lastInvokedActionKeyRef.current = action.key queueMicrotask(() => { if (lastInvokedActionKeyRef.current === action.key) { lastInvokedActionKeyRef.current = null } }) void handleSelect(action) } return ( ) } > {!trigger && } {!trigger && showChevron && Menu} {triggerLabel} {label && {label}} {label && visibleActions.length > 0 && } {visibleActions.length === 0 && ( {emptyLabel} )} {visibleActions.map((action, index) => { const isLoading = action.loading || loadingKey === action.key const previousSection = index > 0 ? visibleActions[index - 1]?.section : undefined const shouldRenderSection = Boolean(action.section && action.section !== previousSection) return ( {shouldRenderSection ? ( <> {index > 0 ? : null} {action.section} ) : null} { if (action.keepOpen || !closeOnSelect) { event.preventDefault() } stopInteractivePropagation(event) triggerAction(action) }} onClick={(event) => { stopInteractivePropagation(event) triggerAction(action) }} onMouseDown={stopInteractivePropagation} onDoubleClick={stopInteractivePropagation} > {renderItem?.(action, { loading: isLoading, close: () => setOpen(false) }) ?? ( <> {isLoading ? : action.icon} {action.label} {action.description ? ( {isLoading ? loadingLabel : action.description} ) : null} {action.shortcut && ( {action.shortcut} )} )} ) })} ) } export { ActionMenu }