// 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)
)}