'use client'; import { useSyncExternalStore, useState } from 'react'; import { useTranslations } from 'next-intl'; import { iconForName, MoreHorizontal, Puzzle } from '@/components/icons'; import { ContextMenu, ContextMenuItem } from '@/components/ui/context-menu'; import { useContextMenu } from '@/hooks/use-context-menu'; import { cn } from '@/lib/utils'; import { actionApplies, actionsFor, badgeText, runAction, subscribeActions, type RegisteredAction, } from '@/lib/plugin-sandbox/actions'; const EMPTY: RegisteredAction[] = []; /** The actions plugins declared for a surface, filtered by folder role or MIME type. */ export function usePluginActions(surface: string, filter: { mailboxRole?: string | null; mimeType?: string | null } = {}) { const all = useSyncExternalStore(subscribeActions, () => actionsFor(surface), () => EMPTY); return all.filter((a) => actionApplies(a, filter)); } interface PluginActionsProps { /** A surface of kind 'action' in lib/plugin-slot-catalog.ts. */ surface: string; /** What the click is about (ids of the message, files, ...). Filtered per plugin before it is sent. */ context: Record; /** Buttons in a toolbar row, or entries in a menu. */ variant?: 'buttons' | 'menu'; mailboxRole?: string | null; mimeType?: string | null; className?: string; /** Icon-only buttons with the label as tooltip (narrow toolbars). */ compact?: boolean; /** No room for buttons: one "more" button that opens the actions as a menu. */ overflow?: boolean; /** Called after a click, e.g. to close the menu the entry sits in. */ onRan?: () => void; } /** * Buttons or menu entries plugins declared with api.ui.setActions. The host * draws them so they match the app; the plugin gets the click. */ export function PluginActions({ surface, context, variant = 'buttons', mailboxRole, mimeType, className, compact, overflow, onRan }: PluginActionsProps) { const actions = usePluginActions(surface, { mailboxRole, mimeType }); const [busy, setBusy] = useState(null); if (actions.length === 0) return null; if (overflow && variant === 'buttons') { return ; } const run = async (action: RegisteredAction) => { const key = `${action.pluginId}:${action.id}`; setBusy(key); try { onRan?.(); await runAction(action, surface, context); } finally { setBusy((current) => (current === key ? null : current)); } }; if (variant === 'menu') { return ( <> {actions.map((action) => ( { void run(action); }} /> ))} ); } return (
{actions.map((action) => { const Icon = iconForName(action.icon) ?? Puzzle; const key = `${action.pluginId}:${action.id}`; return ( ); })}
); } /** The actions of a surface behind one "more" button, for toolbars without room for them. */ function PluginActionsOverflow({ surface, context, mailboxRole, mimeType, className }: Pick) { const t = useTranslations('email_viewer'); const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu(); return (
); }