- <>
-
-
-
-
-
- {data.node?.frozen && (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ handleUpdateOutputHide()}
+ hidden={!!data.node?.outputs![index].hidden}
+ isToolMode={isToolMode}
+ title={title}
+ />
- {Handle}
- >
+
+ {data.node?.frozen && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {
+ //just to trigger the memoization
+ }}
+ />
+
+
+
+
+
+ {Handle}
);
}
+
+export default memo(NodeOutputField);
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx
index 7af939e2e..2a0844f0b 100644
--- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx
@@ -1,9 +1,9 @@
import { getNodeInputColors } from "@/CustomNodes/helpers/get-node-input-colors";
import { getNodeInputColorsName } from "@/CustomNodes/helpers/get-node-input-colors-name";
+import { sortToolModeFields } from "@/CustomNodes/helpers/sort-tool-mode-field";
import getFieldTitle from "@/CustomNodes/utils/get-field-title";
import { scapedJSONStringfy } from "@/utils/reactflowUtils";
import { useMemo } from "react";
-import { sortToolModeFields } from "../..";
import NodeInputField from "../NodeInputField";
const RenderInputParameters = ({
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx
index 896ab726e..79ab3c78e 100644
--- a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx
@@ -1,6 +1,6 @@
import { useDarkStore } from "@/stores/darkStore";
import useFlowStore from "@/stores/flowStore";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Handle, Position } from "reactflow";
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
import {
@@ -10,7 +10,147 @@ import {
import { cn, groupByFamily } from "../../../../utils/utils";
import HandleTooltipComponent from "../HandleTooltipComponent";
-export default function HandleRenderComponent({
+const BASE_HANDLE_STYLES = {
+ width: "32px",
+ height: "32px",
+ top: "50%",
+ position: "absolute" as const,
+ zIndex: 30,
+ background: "transparent",
+ border: "none",
+} as const;
+
+const HandleContent = memo(function HandleContent({
+ isNullHandle,
+ handleColor,
+ accentForegroundColorName,
+ isHovered,
+ openHandle,
+ testIdComplement,
+ title,
+ showNode,
+ left,
+ nodeId,
+ colorName,
+}: {
+ isNullHandle: boolean;
+ handleColor: string;
+ accentForegroundColorName: string;
+ isHovered: boolean;
+ openHandle: boolean;
+ testIdComplement?: string;
+ title: string;
+ showNode: boolean;
+ left: boolean;
+ nodeId: string;
+ colorName?: string[];
+}) {
+ // Restore animation effect
+ useEffect(() => {
+ if ((isHovered || openHandle) && !isNullHandle) {
+ const styleSheet = document.createElement("style");
+ styleSheet.id = `pulse-${nodeId}`;
+ styleSheet.textContent = `
+ @keyframes pulseNeon {
+ 0% {
+ box-shadow: 0 0 0 2px hsl(var(--node-ring)),
+ 0 0 2px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 4px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 6px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 8px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 10px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 15px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 20px hsl(var(--datatype-${colorName?.[0]}));
+ }
+ 50% {
+ box-shadow: 0 0 0 2px hsl(var(--node-ring)),
+ 0 0 4px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 8px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 12px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 16px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 20px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 25px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 30px hsl(var(--datatype-${colorName?.[0]}));
+ }
+ 100% {
+ box-shadow: 0 0 0 2px hsl(var(--node-ring)),
+ 0 0 2px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 4px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 6px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 8px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 10px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 15px hsl(var(--datatype-${colorName?.[0]})),
+ 0 0 20px hsl(var(--datatype-${colorName?.[0]}));
+ }
+ }
+ `;
+ document.head.appendChild(styleSheet);
+
+ return () => {
+ const existingStyle = document.getElementById(`pulse-${nodeId}`);
+ if (existingStyle) {
+ existingStyle.remove();
+ }
+ };
+ }
+ }, [isHovered, openHandle, isNullHandle, nodeId, colorName]);
+
+ const getNeonShadow = useCallback(
+ (color: string, isActive: boolean) => {
+ if (isNullHandle) return "none";
+ if (!isActive) return `0 0 0 3px hsl(var(--${color}))`;
+ return [
+ "0 0 0 1px hsl(var(--border))",
+ `0 0 2px ${color}`,
+ `0 0 4px ${color}`,
+ `0 0 6px ${color}`,
+ `0 0 8px ${color}`,
+ `0 0 10px ${color}`,
+ `0 0 15px ${color}`,
+ `0 0 20px ${color}`,
+ ].join(", ");
+ },
+ [isNullHandle],
+ );
+
+ const contentStyle = useMemo(
+ () => ({
+ background: isNullHandle ? "hsl(var(--border))" : handleColor,
+ width: "10px",
+ height: "10px",
+ transition: "all 0.2s",
+ boxShadow: getNeonShadow(
+ accentForegroundColorName,
+ isHovered || openHandle,
+ ),
+ animation:
+ (isHovered || openHandle) && !isNullHandle
+ ? "pulseNeon 1.1s ease-in-out infinite"
+ : "none",
+ border: isNullHandle ? "2px solid hsl(var(--muted))" : "none",
+ }),
+ [
+ isNullHandle,
+ handleColor,
+ getNeonShadow,
+ accentForegroundColorName,
+ isHovered,
+ openHandle,
+ ],
+ );
+
+ return (
+
+ );
+});
+
+const HandleRenderComponent = memo(function HandleRenderComponent({
left,
nodes,
tooltipTitle = "",
@@ -35,236 +175,184 @@ export default function HandleRenderComponent({
edges: any;
myData: any;
colors: string[];
- setFilterEdge: any;
- showNode: any;
+ setFilterEdge: (edges: any) => void;
+ showNode: boolean;
testIdComplement?: string;
nodeId: string;
colorName?: string[];
}) {
const handleColorName = colorName?.[0] ?? "";
-
const accentColorName = `datatype-${handleColorName}`;
const accentForegroundColorName = `${accentColorName}-foreground`;
- const setHandleDragging = useFlowStore((state) => state.setHandleDragging);
- const setFilterType = useFlowStore((state) => state.setFilterType);
- const handleDragging = useFlowStore((state) => state.handleDragging);
- const filterType = useFlowStore((state) => state.filterType);
+ const [isHovered, setIsHovered] = useState(false);
+ const [openTooltip, setOpenTooltip] = useState(false);
+
+ const {
+ setHandleDragging,
+ setFilterType,
+ handleDragging,
+ filterType,
+ onConnect,
+ } = useFlowStore(
+ useCallback(
+ (state) => ({
+ setHandleDragging: state.setHandleDragging,
+ setFilterType: state.setFilterType,
+ handleDragging: state.handleDragging,
+ filterType: state.filterType,
+ onConnect: state.onConnect,
+ }),
+ [],
+ ),
+ );
+
const dark = useDarkStore((state) => state.dark);
- const onConnect = useFlowStore((state) => state.onConnect);
-
- const handleMouseUp = () => {
- setHandleDragging(undefined);
- document.removeEventListener("mouseup", handleMouseUp);
- };
-
const myId = useMemo(
() => scapedJSONStringfy(proxy ? { ...id, proxy } : id),
[id, proxy],
);
- const getConnection = useMemo(
- () =>
- (semiConnection: {
- source: string | undefined;
- sourceHandle: string | undefined;
- target: string | undefined;
- targetHandle: string | undefined;
- }) => ({
- source: semiConnection.source ?? nodeId,
- sourceHandle: semiConnection.sourceHandle ?? myId,
- target: semiConnection.target ?? nodeId,
- targetHandle: semiConnection.targetHandle ?? myId,
- }),
+ const getConnection = useCallback(
+ (semiConnection: {
+ source?: string;
+ sourceHandle?: string;
+ target?: string;
+ targetHandle?: string;
+ }) => ({
+ source: semiConnection.source ?? nodeId,
+ sourceHandle: semiConnection.sourceHandle ?? myId,
+ target: semiConnection.target ?? nodeId,
+ targetHandle: semiConnection.targetHandle ?? myId,
+ }),
[nodeId, myId],
);
- const sameDraggingNode = useMemo(
- () => (!left ? handleDragging?.target : handleDragging?.source) === nodeId,
- [left, handleDragging, nodeId],
- );
+ const {
+ sameNode,
+ ownHandle,
+ openHandle,
+ filterOpenHandle,
+ filterPresent,
+ currentFilter,
+ isNullHandle,
+ handleColor,
+ } = useMemo(() => {
+ const sameDraggingNode =
+ (!left ? handleDragging?.target : handleDragging?.source) === nodeId;
+ const sameFilterNode =
+ (!left ? filterType?.target : filterType?.source) === nodeId;
- const ownDraggingHandle = useMemo(
- () =>
+ const ownDraggingHandle =
handleDragging &&
(left ? handleDragging?.target : handleDragging?.source) &&
(left ? handleDragging.targetHandle : handleDragging.sourceHandle) ===
- myId,
- [handleDragging, left, myId],
- );
+ myId;
- const sameFilterNode = useMemo(
- () => (!left ? filterType?.target : filterType?.source) === nodeId,
- [left, filterType, nodeId],
- );
-
- const ownFilterHandle = useMemo(
- () =>
+ const ownFilterHandle =
filterType &&
(left ? filterType?.target : filterType?.source) === nodeId &&
- (left ? filterType.targetHandle : filterType.sourceHandle) === myId,
- [filterType, left, myId],
- );
+ (left ? filterType.targetHandle : filterType.sourceHandle) === myId;
- const sameNode = useMemo(
- () => sameDraggingNode || sameFilterNode,
- [sameDraggingNode, sameFilterNode],
- );
- const ownHandle = useMemo(
- () => ownDraggingHandle || ownFilterHandle,
- [ownDraggingHandle, ownFilterHandle],
- );
-
- const draggingOpenHandle = useMemo(
- () =>
+ const draggingOpenHandle =
handleDragging &&
(left ? handleDragging.source : handleDragging.target) &&
!ownDraggingHandle
? isValidConnection(getConnection(handleDragging), nodes, edges)
- : false,
- [handleDragging, left, ownDraggingHandle, getConnection, nodes, edges],
- );
+ : false;
- const filterOpenHandle = useMemo(
- () =>
+ const filterOpenHandle =
filterType &&
(left ? filterType.source : filterType.target) &&
!ownFilterHandle
? isValidConnection(getConnection(filterType), nodes, edges)
- : false,
- [filterType, left, ownFilterHandle, getConnection, nodes, edges],
- );
+ : false;
- const openHandle = useMemo(
- () => filterOpenHandle || draggingOpenHandle,
- [filterOpenHandle, draggingOpenHandle],
- );
+ const openHandle = filterOpenHandle || draggingOpenHandle;
+ const filterPresent = handleDragging || filterType;
- const filterPresent = useMemo(
- () => handleDragging || filterType,
- [handleDragging, filterType],
- );
-
- const currentFilter = useMemo(
- () =>
- left
- ? {
- targetHandle: myId,
- target: nodeId,
- source: undefined,
- sourceHandle: undefined,
- type: tooltipTitle,
- color: handleColorName,
- }
- : {
- sourceHandle: myId,
- source: nodeId,
- target: undefined,
- targetHandle: undefined,
- type: tooltipTitle,
- color: handleColorName,
- },
- [left, myId, nodeId, tooltipTitle, colors],
- );
-
- const isNullHandle = filterPresent && !(openHandle || ownHandle);
-
- const handleColor = useMemo(
- () =>
- isNullHandle
- ? dark
- ? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)"
- : "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)"
- : "conic-gradient(" +
- colorName!
- .concat(colorName![0])
- .map(
- (color, index) =>
- `hsl(var(--datatype-${color}))` +
- " " +
- ((360 / colors.length) * index - 360 / (colors.length * 4)) +
- "deg " +
- ((360 / colors.length) * index + 360 / (colors.length * 4)) +
- "deg",
- )
- .join(" ,") +
- ")",
- [filterPresent, openHandle, ownHandle, dark, colors],
- );
-
- const [isHovered, setIsHovered] = useState(false);
- const [openTooltip, setOpenTooltip] = useState(false);
-
- useEffect(() => {
- if ((isHovered || openHandle) && !isNullHandle) {
- const styleSheet = document.createElement("style");
- styleSheet.id = `pulse-${nodeId}`;
- styleSheet.textContent = `
- @keyframes pulseNeon {
- 0% {
- box-shadow: 0 0 0 2px hsl(var(--node-ring)),
- 0 0 2px hsl(var(--datatype-${colorName![0]})),
- 0 0 4px hsl(var(--datatype-${colorName![0]})),
- 0 0 6px hsl(var(--datatype-${colorName![0]})),
- 0 0 8px hsl(var(--datatype-${colorName![0]})),
- 0 0 10px hsl(var(--datatype-${colorName![0]})),
- 0 0 15px hsl(var(--datatype-${colorName![0]})),
- 0 0 20px hsl(var(--datatype-${colorName![0]}));
- }
- 50% {
- box-shadow: 0 0 0 2px hsl(var(--node-ring)),
- 0 0 4px hsl(var(--datatype-${colorName![0]})),
- 0 0 8px hsl(var(--datatype-${colorName![0]})),
- 0 0 12px hsl(var(--datatype-${colorName![0]})),
- 0 0 16px hsl(var(--datatype-${colorName![0]})),
- 0 0 20px hsl(var(--datatype-${colorName![0]})),
- 0 0 25px hsl(var(--datatype-${colorName![0]})),
- 0 0 30px hsl(var(--datatype-${colorName![0]}));
- }
- 100% {
- box-shadow: 0 0 0 2px hsl(var(--node-ring)),
- 0 0 2px hsl(var(--datatype-${colorName![0]})),
- 0 0 4px hsl(var(--datatype-${colorName![0]})),
- 0 0 6px hsl(var(--datatype-${colorName![0]})),
- 0 0 8px hsl(var(--datatype-${colorName![0]})),
- 0 0 10px hsl(var(--datatype-${colorName![0]})),
- 0 0 15px hsl(var(--datatype-${colorName![0]})),
- 0 0 20px hsl(var(--datatype-${colorName![0]}));
- }
+ const currentFilter = left
+ ? {
+ targetHandle: myId,
+ target: nodeId,
+ source: undefined,
+ sourceHandle: undefined,
+ type: tooltipTitle,
+ color: handleColorName,
}
- `;
- document.head.appendChild(styleSheet);
- }
+ : {
+ sourceHandle: myId,
+ source: nodeId,
+ target: undefined,
+ targetHandle: undefined,
+ type: tooltipTitle,
+ color: handleColorName,
+ };
- // Cleanup function should always be returned
- return () => {
- const existingStyle = document.getElementById(`pulse-${nodeId}`);
- if (existingStyle) {
- existingStyle.remove();
- }
+ const isNullHandle =
+ filterPresent && !(openHandle || ownDraggingHandle || ownFilterHandle);
+
+ const handleColor = isNullHandle
+ ? dark
+ ? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)"
+ : "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)"
+ : "conic-gradient(" +
+ colorName!
+ .concat(colorName![0])
+ .map(
+ (color, index) =>
+ `hsl(var(--datatype-${color}))` +
+ " " +
+ ((360 / colors.length) * index - 360 / (colors.length * 4)) +
+ "deg " +
+ ((360 / colors.length) * index + 360 / (colors.length * 4)) +
+ "deg",
+ )
+ .join(" ,") +
+ ")";
+
+ return {
+ sameNode: sameDraggingNode || sameFilterNode,
+ ownHandle: ownDraggingHandle || ownFilterHandle,
+ openHandle,
+ filterOpenHandle,
+ filterPresent,
+ currentFilter,
+ isNullHandle,
+ handleColor,
};
- }, [isHovered, openHandle, isNullHandle, colors, nodeId]);
+ }, [
+ left,
+ handleDragging,
+ filterType,
+ nodeId,
+ myId,
+ nodes,
+ edges,
+ getConnection,
+ dark,
+ colors,
+ colorName,
+ tooltipTitle,
+ handleColorName,
+ ]);
- const getNeonShadow = (color: string, isHovered: boolean) => {
- if (isNullHandle) return "none";
- if (!isHovered && !openHandle) return `0 0 0 3px hsl(var(--${color}))`;
- return [
- "0 0 0 1px hsl(var(--border))",
- `0 0 2px ${color}`,
- `0 0 4px ${color}`,
- `0 0 6px ${color}`,
- `0 0 8px ${color}`,
- `0 0 10px ${color}`,
- `0 0 15px ${color}`,
- `0 0 20px ${color}`,
- ].join(", ");
- };
+ const handleMouseDown = useCallback(
+ (event: React.MouseEvent) => {
+ if (event.button === 0) {
+ setHandleDragging(currentFilter);
+ const handleMouseUp = () => {
+ setHandleDragging(undefined);
+ document.removeEventListener("mouseup", handleMouseUp);
+ };
+ document.addEventListener("mouseup", handleMouseUp);
+ }
+ },
+ [currentFilter, setHandleDragging],
+ );
- const handleRef = useRef
(null);
- const invisibleDivRef = useRef(null);
-
- const handleClick = () => {
+ const handleClick = useCallback(() => {
setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!));
setFilterType(currentFilter);
if (filterOpenHandle && filterType) {
@@ -272,14 +360,40 @@ export default function HandleRenderComponent({
setFilterType(undefined);
setFilterEdge([]);
}
- };
+ }, [
+ myData,
+ tooltipTitle,
+ left,
+ nodes,
+ setFilterEdge,
+ setFilterType,
+ currentFilter,
+ filterOpenHandle,
+ filterType,
+ onConnect,
+ getConnection,
+ ]);
+
+ const handleMouseEnter = useCallback(() => setIsHovered(true), []);
+ const handleMouseLeave = useCallback(() => setIsHovered(false), []);
+ const handleMouseUp = useCallback(() => setOpenTooltip(false), []);
+ const handleContextMenu = useCallback(
+ (e: React.MouseEvent) => e.preventDefault(),
+ [],
+ );
+
+ // Memoize the validation function
+ const validateConnection = useCallback(
+ (connection: any) => isValidConnection(connection, nodes, edges),
+ [nodes, edges],
+ );
return (
- isValidConnection(connection, nodes, edges)
- }
+ isValidConnection={validateConnection}
className={cn(
`group/handle z-50 transition-all`,
!showNode && "no-show",
)}
+ style={BASE_HANDLE_STYLES}
onClick={handleClick}
- onMouseUp={() => {
- setOpenTooltip(false);
- }}
- onContextMenu={(event) => {
- event.preventDefault();
- }}
- onMouseDown={(event) => {
- if (event.button === 0) {
- setHandleDragging(currentFilter);
- document.addEventListener("mouseup", handleMouseUp);
- }
- }}
- style={{
- width: "32px",
- height: "32px",
- top: "50%",
- position: "absolute",
- zIndex: 30,
- background: "transparent",
- border: "none",
- }}
- onMouseEnter={() => setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
+ onMouseUp={handleMouseUp}
+ onContextMenu={handleContextMenu}
+ onMouseDown={handleMouseDown}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ data-testid={`handle-${testIdComplement}-${title.toLowerCase()}-${
+ !showNode ? (left ? "target" : "source") : left ? "left" : "right"
+ }`}
>
- setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
- onContextMenu={(event) => {
- event.preventDefault();
- }}
+
);
-}
+});
+
+export default HandleRenderComponent;
diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx
index 84e117a34..2795f3dd9 100644
--- a/src/frontend/src/CustomNodes/GenericNode/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx
@@ -1,7 +1,7 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
-import { useEffect, useMemo, useState } from "react";
+import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useUpdateNodeInternals } from "reactflow";
import { Button } from "../../components/ui/button";
@@ -15,7 +15,7 @@ import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useShortcutsStore } from "../../stores/shortcuts";
import { useTypesStore } from "../../stores/typesStore";
-import { OutputFieldType, VertexBuildTypeAPI } from "../../types/api";
+import { VertexBuildTypeAPI } from "../../types/api";
import { NodeDataType } from "../../types/flow";
import { checkHasToolMode } from "../../utils/reactflowUtils";
import { classNames, cn } from "../../utils/utils";
@@ -23,7 +23,6 @@ import { classNames, cn } from "../../utils/utils";
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
import useCheckCodeValidity from "../hooks/use-check-code-validity";
import useUpdateNodeCode from "../hooks/use-update-node-code";
-import sortFields from "../utils/sort-fields";
import NodeDescription from "./components/NodeDescription";
import NodeName from "./components/NodeName";
import { OutputParameter } from "./components/NodeOutputParameter";
@@ -32,27 +31,36 @@ import RenderInputParameters from "./components/RenderInputParameters";
import { NodeIcon } from "./components/nodeIcon";
import { useBuildStatus } from "./hooks/use-get-build-status";
-export const sortToolModeFields = (
- a: string,
- b: string,
- template: any,
- fieldOrder: string[],
- isToolMode: boolean,
-) => {
- if (!isToolMode) return sortFields(a, b, fieldOrder);
+const MemoizedOutputParameter = memo(OutputParameter);
+const MemoizedRenderInputParameters = memo(RenderInputParameters);
+const MemoizedNodeIcon = memo(NodeIcon);
+const MemoizedNodeName = memo(NodeName);
+const MemoizedNodeStatus = memo(NodeStatus);
+const MemoizedNodeDescription = memo(NodeDescription);
- const aToolMode = template[a]?.tool_mode ?? false;
- const bToolMode = template[b]?.tool_mode ?? false;
+const HiddenOutputsButton = memo(
+ ({
+ showHiddenOutputs,
+ onClick,
+ }: {
+ showHiddenOutputs: boolean;
+ onClick: () => void;
+ }) => (
+
+ ),
+);
- // If one is tool_mode and the other isn't, tool_mode goes last
- if (aToolMode && !bToolMode) return 1;
- if (!aToolMode && bToolMode) return -1;
-
- // If both are tool_mode or both aren't, use regular field order
- return sortFields(a, b, fieldOrder);
-};
-
-export default function GenericNode({
+function GenericNode({
data,
selected,
}: {
@@ -61,6 +69,14 @@ export default function GenericNode({
xPos?: number;
yPos?: number;
}): JSX.Element {
+ const [isOutdated, setIsOutdated] = useState(false);
+ const [isUserEdited, setIsUserEdited] = useState(false);
+ const [borderColor, setBorderColor] = useState("");
+ const [loadingUpdate, setLoadingUpdate] = useState(false);
+ const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
+ const [validationStatus, setValidationStatus] =
+ useState(null);
+
const types = useTypesStore((state) => state.types);
const templates = useTypesStore((state) => state.templates);
const deleteNode = useFlowStore((state) => state.deleteNode);
@@ -68,11 +84,19 @@ export default function GenericNode({
const updateNodeInternals = useUpdateNodeInternals();
const setErrorData = useAlertStore((state) => state.setErrorData);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
- const [isOutdated, setIsOutdated] = useState(false);
- const [isUserEdited, setIsUserEdited] = useState(false);
- const [borderColor, setBorderColor] = useState("");
+ const edges = useFlowStore((state) => state.edges);
+ const shortcuts = useShortcutsStore((state) => state.shortcuts);
+ const buildStatus = useBuildStatus(data, data.id);
+
const showNode = data.showNode ?? true;
+ const getValidationStatus = (data) => {
+ setValidationStatus(data);
+ return null;
+ };
+
+ const { mutate: validateComponentCode } = usePostValidateComponentCode();
+
const updateNodeCode = useUpdateNodeCode(
data?.id,
data.node!,
@@ -82,6 +106,8 @@ export default function GenericNode({
updateNodeInternals,
);
+ useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
+
if (!data.node!.template) {
setErrorData({
title: `Error in component ${data.node!.display_name}`,
@@ -94,23 +120,11 @@ export default function GenericNode({
deleteNode(data.id);
}
- useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
-
- const [loadingUpdate, setLoadingUpdate] = useState(false);
-
- const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
-
- const { mutate: validateComponentCode } = usePostValidateComponentCode();
-
- const edges = useFlowStore((state) => state.edges);
-
- const handleUpdateCode = () => {
+ const handleUpdateCode = useCallback(() => {
setLoadingUpdate(true);
takeSnapshot();
- // to update we must get the code from the templates in useTypesStore
+
const thisNodeTemplate = templates[data.type]?.template;
- // if the template does not have a code key
- // return
if (!thisNodeTemplate?.code) return;
const currentCode = thisNodeTemplate.code.value;
@@ -125,70 +139,93 @@ export default function GenericNode({
edges,
data.id,
);
-
updateNodeCode(newNode, currentCode, "code", type);
setLoadingUpdate(false);
}
},
onError: (error) => {
setErrorData({
- title: "Error updating Compoenent code",
+ title: "Error updating Component code",
list: [
"There was an error updating the Component.",
"If the error persists, please report it on our Discord or GitHub.",
],
});
- console.log(error);
+ console.error(error);
setLoadingUpdate(false);
},
},
);
}
- };
+ }, [
+ data,
+ templates,
+ edges,
+ updateNodeCode,
+ validateComponentCode,
+ setErrorData,
+ takeSnapshot,
+ ]);
- function handleUpdateCodeWShortcut() {
+ const handleUpdateCodeWShortcut = useCallback(() => {
if (isOutdated && selected) {
handleUpdateCode();
}
- }
-
- const shownOutputs =
- data.node!.outputs?.filter((output) => !output.hidden) ?? [];
-
- const hiddenOutputs =
- data.node!.outputs?.filter((output) => output.hidden) ?? [];
+ }, [isOutdated, selected, handleUpdateCode]);
const update = useShortcutsStore((state) => state.update);
useHotkeys(update, handleUpdateCodeWShortcut, { preventDefault: true });
- const shortcuts = useShortcutsStore((state) => state.shortcuts);
+ // Memoized values
+ const isToolMode = useMemo(
+ () =>
+ data.node?.outputs?.some(
+ (output) => output.name === "component_as_tool",
+ ) ?? false,
+ [data.node?.outputs],
+ );
- const [openShowMoreOptions, setOpenShowMoreOptions] = useState(false);
+ const hasToolMode = useMemo(
+ () => checkHasToolMode(data.node?.template ?? {}),
+ [data.node?.template],
+ );
- const renderOutputs = (outputs) => {
- return outputs.map((output, idx) => (
- out.name === output.name) ??
- idx
- }
- lastOutput={idx === outputs.length - 1}
- data={data}
- types={types}
- selected={selected}
- showNode={showNode}
- isToolMode={isToolMode}
- />
- ));
- };
+ const hasOutputs = useMemo(
+ () => data.node?.outputs && data.node.outputs.length > 0,
+ [data.node?.outputs],
+ );
- useEffect(() => {
- if (hiddenOutputs && hiddenOutputs.length == 0) {
- setShowHiddenOutputs(false);
- }
- }, [hiddenOutputs]);
+ const renderOutputs = useCallback(
+ (outputs, key?: string) => {
+ return outputs?.map((output, idx) => (
+ out.name === output.name) ??
+ idx
+ }
+ lastOutput={idx === outputs.length - 1}
+ data={data}
+ types={types}
+ selected={selected}
+ showNode={showNode}
+ isToolMode={isToolMode}
+ />
+ ));
+ },
+ [data, types, selected, showNode, isToolMode],
+ );
+
+ const { shownOutputs, hiddenOutputs } = useMemo(
+ () => ({
+ shownOutputs:
+ data.node?.outputs?.filter((output) => !output.hidden) ?? [],
+ hiddenOutputs:
+ data.node?.outputs?.filter((output) => output.hidden) ?? [],
+ }),
+ [data.node?.outputs],
+ );
const memoizedNodeToolbarComponent = useMemo(() => {
return selected ? (
@@ -211,7 +248,6 @@ export default function GenericNode({
onCloseAdvancedModal={() => {}}
updateNode={handleUpdateCode}
isOutdated={isOutdated && isUserEdited}
- setOpenShowMoreOptions={setOpenShowMoreOptions}
/>
) : (
@@ -230,20 +266,95 @@ export default function GenericNode({
shortcuts,
]);
- const isToolMode =
- data.node?.outputs?.some((output) => output.name === "component_as_tool") ??
- false;
+ useEffect(() => {
+ if (hiddenOutputs && hiddenOutputs.length === 0) {
+ setShowHiddenOutputs(false);
+ }
+ }, [hiddenOutputs]);
- const buildStatus = useBuildStatus(data, data.id);
- const hasOutputs = data.node?.outputs && data.node?.outputs.length > 0;
- const [validationStatus, setValidationStatus] =
- useState(null);
- const getValidationStatus = (data) => {
- setValidationStatus(data);
- return null;
- };
+ const renderNodeIcon = useCallback(() => {
+ return (
+
+ );
+ }, [data.type, showNode, data.node?.icon, data.node?.flow, hasToolMode]);
- const hasToolMode = checkHasToolMode(data.node?.template ?? {});
+ const renderNodeName = useCallback(() => {
+ return (
+
+ );
+ }, [
+ data.node?.display_name,
+ data.id,
+ selected,
+ showNode,
+ validationStatus,
+ isOutdated,
+ data.node?.beta,
+ ]);
+
+ const renderNodeStatus = useCallback(() => {
+ return (
+
+ );
+ }, [
+ data,
+ showNode,
+ selected,
+ buildStatus,
+ isOutdated,
+ isUserEdited,
+ getValidationStatus,
+ ]);
+
+ const renderDescription = useCallback(() => {
+ return (
+
+ );
+ }, [data.node?.description, data.id, selected]);
+
+ const renderInputParameters = useCallback(() => {
+ return (
+
+ );
+ }, [data, types, isToolMode, showNode, shownOutputs, showHiddenOutputs]);
return (
@@ -298,78 +409,27 @@ export default function GenericNode({
className={"generic-node-title-arrangement"}
data-testid="generic-node-title-arrangement"
>
-
-
-
-
+ {renderNodeIcon()}
+
{renderNodeName()}
{!showNode && (
<>
-
+ {renderInputParameters()}
{shownOutputs &&
shownOutputs.length > 0 &&
- renderOutputs(shownOutputs)}
+ renderOutputs(shownOutputs, "render-outputs")}
>
)}
-
+ {renderNodeStatus()}
- {showNode && (
-