113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
/**
|
|
* `PluginLayoutCellView` — renders a `customPluginLayout` top-level
|
|
* {@link LayoutNode} (ticket #43, F4, carnet v2 §3): the plugin's registered
|
|
* React component when its provider is loaded and declares the type,
|
|
* otherwise {@link PluginLayoutFallback}.
|
|
*
|
|
* State is opaque to the domain (`unknown`) and round-trips through
|
|
* `LayoutGateway` exactly like any other node field — `setState` here is the
|
|
* only way a plugin component is meant to persist it (never direct gateway
|
|
* calls from inside a plugin component).
|
|
*/
|
|
|
|
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
|
|
import type { CustomPluginLayoutCell, PluginAdmin } from "@/domain";
|
|
import { usePluginRuntime } from "./PluginRuntimeProvider";
|
|
import { PluginLayoutFallback } from "./PluginLayoutFallback";
|
|
import { resolvePluginLayoutAvailability } from "./layoutAvailability";
|
|
|
|
interface PluginLayoutCellViewProps {
|
|
projectId: string;
|
|
cell: CustomPluginLayoutCell;
|
|
installedPlugins?: PluginAdmin[];
|
|
onStateChange: (nextState: unknown) => void;
|
|
onOpenPlugins: () => void;
|
|
onChooseAnotherLayout: () => void;
|
|
}
|
|
|
|
interface PluginLayoutErrorBoundaryProps {
|
|
resetKey: string;
|
|
fallback: ReactNode;
|
|
children: ReactNode;
|
|
}
|
|
|
|
class PluginLayoutErrorBoundary extends Component<
|
|
PluginLayoutErrorBoundaryProps,
|
|
{ hasError: boolean }
|
|
> {
|
|
state = { hasError: false };
|
|
|
|
static getDerivedStateFromError(): { hasError: boolean } {
|
|
return { hasError: true };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
console.error("[plugin-layout] render failed", error, errorInfo);
|
|
}
|
|
|
|
componentDidUpdate(prevProps: PluginLayoutErrorBoundaryProps): void {
|
|
if (prevProps.resetKey !== this.props.resetKey && this.state.hasError) {
|
|
this.setState({ hasError: false });
|
|
}
|
|
}
|
|
|
|
render(): ReactNode {
|
|
if (this.state.hasError) return this.props.fallback;
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
export function PluginLayoutCellView({
|
|
projectId,
|
|
cell,
|
|
installedPlugins,
|
|
onStateChange,
|
|
onOpenPlugins,
|
|
onChooseAnotherLayout,
|
|
}: PluginLayoutCellViewProps) {
|
|
const { registry } = usePluginRuntime();
|
|
const availability = resolvePluginLayoutAvailability(registry, cell, installedPlugins);
|
|
const providerDisplayName =
|
|
registry.get(cell.pluginId)?.displayName ??
|
|
installedPlugins?.find((p) => p.id === cell.pluginId)?.displayName;
|
|
|
|
if (availability !== "available") {
|
|
return (
|
|
<PluginLayoutFallback
|
|
cell={cell}
|
|
availability={availability}
|
|
providerDisplayName={providerDisplayName}
|
|
onOpenPlugins={onOpenPlugins}
|
|
onChooseAnotherLayout={onChooseAnotherLayout}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const Component = registry.layoutComponent(cell.pluginId, cell.layoutType)!;
|
|
const fallback = (
|
|
<PluginLayoutFallback
|
|
cell={cell}
|
|
availability="incompatible"
|
|
providerDisplayName={providerDisplayName}
|
|
onOpenPlugins={onOpenPlugins}
|
|
onChooseAnotherLayout={onChooseAnotherLayout}
|
|
/>
|
|
);
|
|
return (
|
|
<PluginLayoutErrorBoundary
|
|
resetKey={`${cell.pluginId}:${cell.layoutType}:${cell.id}`}
|
|
fallback={fallback}
|
|
>
|
|
<Component
|
|
projectId={projectId}
|
|
nodeId={cell.id}
|
|
layoutType={cell.layoutType}
|
|
state={cell.state}
|
|
setState={onStateChange}
|
|
availability="available"
|
|
/>
|
|
</PluginLayoutErrorBoundary>
|
|
);
|
|
}
|