/** * @file Node inspector component */ import * as React from "react"; import type { Node } from "../../../types/core"; import type { InspectorRenderProps } from "../../../types/NodeDefinition"; import { useNodeEditor } from "../../../contexts/composed/node-editor/context"; import { useEditorActionState } from "../../../contexts/composed/EditorActionStateContext"; import { useNodeDefinition } from "../../../contexts/node-definitions/hooks/useNodeDefinition"; import { useExternalDataRef } from "../../../contexts/external-data/ExternalDataContext"; import { useExternalData } from "../../../contexts/external-data/useExternalData"; import { NodeBehaviorInspector } from "./NodeBehaviorInspector"; import { NodeActionsBehaviorInspector } from "./NodeActionsBehaviorInspector"; import { GroupBehaviorInspector } from "./GroupBehaviorInspector"; import styles from "./NodeInspector.module.css"; import { calculateAlignmentPositions } from "../../controls/alignments/utils"; import type { AlignmentActionType } from "../../controls/alignments/types"; import { getBehaviors, behaviorArrayIncludes } from "../../../types/behaviors"; import { hasNodeIdentityChanged, hasNodeStateChanged } from "../../../core/node/comparators"; export type NodeInspectorProps = { node: Node; }; export const NodeInspector: React.FC = React.memo( ({ node }) => { const { state: nodeEditorState, actions: nodeEditorActions } = useNodeEditor(); const { state: actionState } = useEditorActionState(); const nodeDefinition = useNodeDefinition(node.type); const externalDataRef = useExternalDataRef(node.id); const externalDataState = useExternalData(node, externalDataRef); // Handle node updates - Memoized to prevent recreation on every render const handleUpdateNode = React.useCallback( (updates: Partial) => { nodeEditorActions.updateNode(node.id, updates); }, [node.id, nodeEditorActions], ); // Handle external data updates const handleUpdateExternalData = React.useCallback( async (data: unknown) => { if (externalDataState.update) { await externalDataState.update(data); } }, [externalDataState.update], ); // Handle node deletion const handleDeleteNode = React.useCallback(() => { nodeEditorActions.deleteNode(node.id); }, [node.id, nodeEditorActions]); // Get all selected nodes for alignment const selectedNodes = actionState.selectedNodeIds.map((id) => nodeEditorState.nodes[id]).filter(Boolean); // Handle alignment operations const handleAlignNodes = React.useCallback( (alignmentType: AlignmentActionType, nodes: Node[]) => { const positionUpdates = calculateAlignmentPositions(nodes, alignmentType); if (Object.keys(positionUpdates).length > 0) { nodeEditorActions.moveNodes(positionUpdates); } }, [nodeEditorActions], ); // Inspector render props - Only recreate when dependencies actually change const inspectorProps = React.useMemo( () => ({ node, externalData: externalDataState.data, isLoadingExternalData: externalDataState.isLoading, externalDataError: externalDataState.error, onUpdateNode: handleUpdateNode, onUpdateExternalData: handleUpdateExternalData, onDeleteNode: handleDeleteNode, selectedNodes, onAlignNodes: handleAlignNodes, }), [ node, externalDataState.data, externalDataState.isLoading, externalDataState.error, handleUpdateNode, handleUpdateExternalData, handleDeleteNode, selectedNodes, handleAlignNodes, ], ); const behaviors = React.useMemo(() => { return getBehaviors(nodeDefinition); }, [nodeDefinition]); // Check if a function is likely a React component (by naming convention) const isReactComponent = React.useCallback( (fn: (props: InspectorRenderProps>) => React.ReactElement): boolean => { // React components should start with an uppercase letter return /^[A-Z]/.test(fn.name || ""); }, [], ); // Render custom inspector with proper component handling const customInspectorElement = React.useMemo(() => { if (!nodeDefinition?.renderInspector) { return null; } const renderFn = nodeDefinition.renderInspector; // If it looks like a React component (starts with uppercase), use as JSX // This allows hooks to work correctly if (isReactComponent(renderFn)) { const CustomInspector = renderFn; return ; } // Otherwise, call as a regular function (legacy support) return renderFn(inspectorProps); }, [nodeDefinition?.renderInspector, inspectorProps, isReactComponent]); return ( <> {/* Custom inspector (node-specific) */} {customInspectorElement &&
{customInspectorElement}
} {/* Behavior-based inspectors */} {behaviorArrayIncludes(behaviors, "node") && (
)} {behaviorArrayIncludes(behaviors, "node") && (
)} {behaviorArrayIncludes(behaviors, "group") && (
)} ); }, (prevProps, nextProps) => { // Custom comparison function for memo // Only re-render if the node ID changes or critical node properties change if (hasNodeIdentityChanged(prevProps.node, nextProps.node)) { return false; } if (hasNodeStateChanged(prevProps.node, nextProps.node)) { return false; } // Reference equality for data and position (intentional for performance) return ( prevProps.node.data === nextProps.node.data && prevProps.node.position === nextProps.node.position && prevProps.node.visible === nextProps.node.visible ); }, ); NodeInspector.displayName = "NodeInspector";