// RowActionsMenu — el «…» de una fila: la acción primaria (si hay) arriba y // destacada, las principales debajo y las secundarias (compartir, imprimir, // correo, chat, PDF/XML y lo que aporten addons vía registerRecordAction) // agrupadas en un «Más…» discreto (o en el primer nivel con groupSecondary={false}). Una acción por capacidad sin proveedor // activo, o una contribución cuyo addon no está instalado, no aparece. // // DynamicTable lo usa por defecto (getDynamicColumns) y los hosts que arman su // propia columna de acciones lo reutilizan en vez de duplicar el menú. import * as React from 'react' import { useTranslation } from 'react-i18next' import { MoreHorizontal } from 'lucide-react' // Root entry on purpose (not '/primitives'): see dropdown-menu-entry.test.ts. import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@asteby/metacore-ui' import { DynamicIcon } from './dynamic-icon' import { translateMetadataLabel } from './dynamic-columns-helpers' import { RequiresAddonLock, resolveRequiresAddon, useRequiresAddonLabel } from './requires-addon' import { useInstalledAddons } from './installed-addons-context' import { isActionProviderActive, splitActionsByPriority, useRecordActions, type ResolvedRecordAction } from './record-actions' /** * One entry of the row "…" menu. An action gated by a missing optional addon * (`requiresAddon`) stays listed with a lock + tooltip; the click still goes to * `onAction`, whose shared handler (useDynamicRowActions) opens the * requires-addon dialog instead of executing. */ export function RowActionMenuItem({ action, label, onSelect, className, }: { action: any label: React.ReactNode onSelect: (e: React.MouseEvent) => void className?: string }) { const requirement = resolveRequiresAddon(action) const requiresAddonLabel = useRequiresAddonLabel() return ( {label} {requirement && } ) } export interface RowActionsMenuProps { /** Acciones de metadata ya filtradas por estado/condición de la fila. */ actions: any[] row: Record /** Modelo de la fila: resuelve las acciones aportadas por addons. */ model?: string onAction?: (actionKey: string, row: any) => void /** Sustituye el ícono del disparador (p. ej. un spinner mientras imprime). */ triggerIcon?: React.ReactNode disabled?: boolean /** * Agrupa las acciones secundarias en un submenú «Más…» (por defecto). Con * `false` todas se listan en el primer nivel: las secundarias van después * de un separador, sin submenú. */ groupSecondary?: boolean } /** * Modelo de las filas que pinta la tabla. DynamicTable lo provee: la factoría * de columnas (getDynamicColumns) no recibe el modelo y el menú lo necesita * para resolver las acciones aportadas por addons. */ export const RowActionsModelContext = React.createContext(undefined) type MenuEntry = | { kind: 'action'; action: any } | { kind: 'contribution'; item: ResolvedRecordAction } export function RowActionsMenu({ actions, row, model, onAction, triggerIcon, disabled, groupSecondary = true }: RowActionsMenuProps) { const { t } = useTranslation() const installed = useInstalledAddons() const ctxModel = React.useContext(RowActionsModelContext) const recordModel = model ?? ctxModel const contributed = useRecordActions(recordModel ?? '', recordModel ? row : null).filter((c) => !!c.run) const available = actions.filter((a) => isActionProviderActive(a, installed)) const { primary, main, secondary } = splitActionsByPriority(available) const mainEntries: MenuEntry[] = main.map((action) => ({ kind: 'action', action })) const secondaryEntries: MenuEntry[] = secondary.map((action) => ({ kind: 'action', action })) for (const item of contributed) { ;(item.priority === 'primary' ? mainEntries : secondaryEntries).push({ kind: 'contribution', item }) } if (!primary && mainEntries.length === 0 && secondaryEntries.length === 0) return null const renderEntry = (e: MenuEntry) => e.kind === 'action' ? ( onAction?.(e.action.key, row)} /> ) : ( void e.item.run?.()}> {t(e.item.label, { defaultValue: e.item.label })} ) const hasMain = !!primary || mainEntries.length > 0 const moreLabel = t('datatable.more_actions', { defaultValue: 'Más…' }) return (
e.stopPropagation()}> {primary ? ( onAction?.(primary.key, row)} className="font-semibold" /> ) : null} {mainEntries.map(renderEntry)} {secondaryEntries.length > 0 && hasMain && !groupSecondary ? ( <> {secondaryEntries.map(renderEntry)} ) : secondaryEntries.length > 0 && hasMain ? ( <> {moreLabel} {secondaryEntries.map(renderEntry)} ) : ( // Sin principales no hay con qué competir: lista plana. secondaryEntries.map(renderEntry) )}
) }