Files
fusion/packages/dashboard/app/components/PluginSlot.tsx
Fusion 513cab6fc4 feat(FN-1925): add plugin UI slot infrastructure
- Add fetchPluginUiSlots API wrapper with PluginUiSlotEntry type
- Add usePluginUiSlots hook with 30-second TTL cache for slot data
- Add PluginSlot component with error boundary and placeholder rendering
- Add comprehensive tests for hook (cache behavior, error handling, isolation)
- Add comprehensive tests for component (render states, error boundary)
2026-04-16 10:04:44 -07:00

53 lines
1.6 KiB
TypeScript

import { ErrorBoundary } from "./ErrorBoundary";
import { usePluginUiSlots } from "../hooks/usePluginUiSlots";
interface PluginSlotProps {
/** The slot identifier to render (e.g., "task-detail-tab", "header-action") */
slotId: string;
/** Optional project ID for multi-project slot scoping */
projectId?: string;
}
/**
* Renders placeholder divs for all plugin UI slots matching the given slotId.
*
* This component is non-critical: it silently fails (returns null) for loading
* errors, or when no plugins are registered for the slot. Each rendered slot
* is wrapped in an ErrorBoundary to isolate plugin rendering failures from the
* parent dashboard UI.
*
* Future iterations will replace placeholder divs with dynamically loaded
* components via the plugin's componentPath.
*/
export function PluginSlot({ slotId, projectId }: PluginSlotProps): JSX.Element | null {
const { getSlotsForId, loading, error } = usePluginUiSlots(projectId);
// Non-critical failure — no visible UI when loading, errored, or no matching slots
if (loading || error || !slotId) {
return null;
}
const matchingEntries = getSlotsForId(slotId);
if (matchingEntries.length === 0) {
return null;
}
return (
<ErrorBoundary level="page">
<>
{matchingEntries.map((entry) => (
<div
key={`${entry.pluginId}-${entry.slot.slotId}`}
data-plugin-slot
data-slot-id={entry.slot.slotId}
data-plugin-id={entry.pluginId}
data-component-path={entry.slot.componentPath}
aria-label={entry.slot.label}
/>
))}
</>
</ErrorBoundary>
);
}