diff --git a/src/frontend/src/CustomNodes/NoteNode/NoteToolbarComponent/index.tsx b/src/frontend/src/CustomNodes/NoteNode/NoteToolbarComponent/index.tsx
index 160da7b58b1b..c1bb0746bea7 100644
--- a/src/frontend/src/CustomNodes/NoteNode/NoteToolbarComponent/index.tsx
+++ b/src/frontend/src/CustomNodes/NoteNode/NoteToolbarComponent/index.tsx
@@ -1,18 +1,11 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
-import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
-import {
- Select,
- SelectContentWithoutPortal,
- SelectItem,
- SelectTrigger,
-} from "@/components/ui/select-custom";
+import { Select, SelectTrigger } from "@/components/ui/select-custom";
import { COLOR_OPTIONS } from "@/constants/constants";
-import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem";
import useAlertStore from "@/stores/alertStore";
import useFlowStore from "@/stores/flowStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
@@ -20,9 +13,12 @@ import { useShortcutsStore } from "@/stores/shortcuts";
import { noteDataType } from "@/types/flow";
import { classNames, cn, openInNewTab } from "@/utils/utils";
import { cloneDeep } from "lodash";
+import { memo, useCallback, useMemo } from "react";
import IconComponent from "../../../components/common/genericIconComponent";
+import { ColorPickerButtons } from "../components/color-picker-buttons";
+import { SelectItems } from "../components/select-items";
-export default function NoteToolbarComponent({
+const NoteToolbarComponent = memo(function NoteToolbarComponent({
data,
bgColor,
}: {
@@ -30,191 +26,142 @@ export default function NoteToolbarComponent({
bgColor: string;
}) {
const setNoticeData = useAlertStore((state) => state.setNoticeData);
- const nodes = useFlowStore((state) => state.nodes);
- const setLastCopiedSelection = useFlowStore(
- (state) => state.setLastCopiedSelection,
- );
- const paste = useFlowStore((state) => state.paste);
- const shortcuts = useShortcutsStore((state) => state.shortcuts);
+
+ // Combine multiple store selectors into one to reduce re-renders
+ const { nodes, setLastCopiedSelection, paste, setNode, deleteNode } =
+ useFlowStore(
+ useCallback(
+ (state) => ({
+ nodes: state.nodes,
+ setLastCopiedSelection: state.setLastCopiedSelection,
+ paste: state.paste,
+ setNode: state.setNode,
+ deleteNode: state.deleteNode,
+ }),
+ [],
+ ),
+ );
+
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
- const deleteNode = useFlowStore((state) => state.deleteNode);
- const setNode = useFlowStore((state) => state.setNode);
+ const shortcuts = useShortcutsStore((state) => state.shortcuts);
- function openDocs() {
+ const openDocs = useCallback(() => {
if (data.node?.documentation) {
return openInNewTab(data.node?.documentation);
}
setNoticeData({
title: `${data.id} docs is not available at the moment.`,
});
- }
+ }, [data.node?.documentation, data.id, setNoticeData]);
+
+ const handleSelectChange = useCallback(
+ (event: string) => {
+ switch (event) {
+ case "documentation":
+ openDocs();
+ break;
+ case "delete":
+ takeSnapshot();
+ deleteNode(data.id);
+ break;
+ case "copy":
+ const node = nodes.filter((node) => node.id === data.id);
+ setLastCopiedSelection({ nodes: cloneDeep(node), edges: [] });
+ break;
+ case "duplicate":
+ const targetNode = nodes.find((node) => node.id === data.id);
+ if (targetNode) {
+ paste(
+ {
+ nodes: [targetNode],
+ edges: [],
+ },
+ {
+ x: 50,
+ y: 10,
+ paneX: targetNode.position.x,
+ paneY: targetNode.position.y,
+ },
+ );
+ }
+ break;
+ }
+ },
+ [
+ openDocs,
+ takeSnapshot,
+ deleteNode,
+ data.id,
+ nodes,
+ setLastCopiedSelection,
+ paste,
+ ],
+ );
+
+ // Memoize the color picker background style
+ const colorPickerStyle = useMemo(
+ () => ({
+ backgroundColor: COLOR_OPTIONS[bgColor] ?? "#00000000",
+ }),
+ [bgColor],
+ );
- const handleSelectChange = (event) => {
- switch (event) {
- case "documentation":
- openDocs();
- break;
- case "delete":
- takeSnapshot();
- deleteNode(data.id);
- break;
- case "copy":
- const node = nodes.filter((node) => node.id === data.id);
- setLastCopiedSelection({ nodes: cloneDeep(node), edges: [] });
- break;
- case "duplicate":
- paste(
- {
- nodes: [nodes.find((node) => node.id === data.id)!],
- edges: [],
- },
- {
- x: 50,
- y: 10,
- paneX: nodes.find((node) => node.id === data.id)?.position.x,
- paneY: nodes.find((node) => node.id === data.id)?.position.y,
- },
- );
- break;
- }
- };
- // the deafult value is allways the first one if none is provided
return (
- <>
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
- {Object.entries(COLOR_OPTIONS).map(([color, code]) => {
- return (
-
- );
- })}
-
-
-
-
+
+
+
+
+
+
);
-}
+});
+
+NoteToolbarComponent.displayName = "NoteToolbarComponent";
+
+export default NoteToolbarComponent;
diff --git a/src/frontend/src/CustomNodes/NoteNode/components/color-picker-buttons.tsx b/src/frontend/src/CustomNodes/NoteNode/components/color-picker-buttons.tsx
new file mode 100644
index 000000000000..5d9ede5808f6
--- /dev/null
+++ b/src/frontend/src/CustomNodes/NoteNode/components/color-picker-buttons.tsx
@@ -0,0 +1,56 @@
+import { Button } from "@/components/ui/button";
+import { COLOR_OPTIONS } from "@/constants/constants";
+import { noteDataType } from "@/types/flow";
+import { cn } from "@/utils/utils";
+
+import { memo } from "react";
+
+export const ColorPickerButtons = memo(
+ ({
+ bgColor,
+ data,
+ setNode,
+ }: {
+ bgColor: string;
+ data: noteDataType;
+ setNode: (id: string, updater: any) => void;
+ }) => (
+
+ {Object.entries(COLOR_OPTIONS).map(([color, code]) => (
+
+ ))}
+
+ ),
+);
+
+ColorPickerButtons.displayName = "ColorPickerButtons";
diff --git a/src/frontend/src/CustomNodes/NoteNode/components/select-items.tsx b/src/frontend/src/CustomNodes/NoteNode/components/select-items.tsx
new file mode 100644
index 000000000000..041c3691a7e2
--- /dev/null
+++ b/src/frontend/src/CustomNodes/NoteNode/components/select-items.tsx
@@ -0,0 +1,60 @@
+import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
+import { SelectItem } from "@/components/ui/select";
+import { SelectContentWithoutPortal } from "@/components/ui/select-custom";
+import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem";
+import { noteDataType } from "@/types/flow";
+
+import { memo } from "react";
+
+export const SelectItems = memo(
+ ({ shortcuts, data }: { shortcuts: any[]; data: noteDataType }) => (
+
+
+ obj.name === "Duplicate")?.shortcut!
+ }
+ value="Duplicate"
+ icon="Copy"
+ dataTestId="copy-button-modal"
+ />
+
+
+ obj.name === "Copy")?.shortcut!}
+ value="Copy"
+ icon="Clipboard"
+ dataTestId="copy-button-modal"
+ />
+
+
+ obj.name === "Docs")?.shortcut!}
+ value="Docs"
+ icon="FileText"
+ dataTestId="docs-button-modal"
+ />
+
+
+
+
+ Delete
+
+
+
+
+
+
+ ),
+);
+
+SelectItems.displayName = "SelectItems";
diff --git a/src/frontend/src/components/common/shadTooltipComponent/index.tsx b/src/frontend/src/components/common/shadTooltipComponent/index.tsx
index e790d816af21..3ed2c74f53ec 100644
--- a/src/frontend/src/components/common/shadTooltipComponent/index.tsx
+++ b/src/frontend/src/components/common/shadTooltipComponent/index.tsx
@@ -1,54 +1,97 @@
-import React, { forwardRef } from "react";
+import React, { forwardRef, memo, useMemo } from "react";
import { ShadToolTipType } from "../../../types/components";
import { cn } from "../../../utils/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
-const ShadTooltip = forwardRef
(
- (
+// Extract static styles
+const BASE_TOOLTIP_CLASSES =
+ "z-[99] max-w-96 bg-tooltip text-[12px] text-tooltip-foreground";
+
+// Memoize the tooltip content component
+const MemoizedTooltipContent = memo(
+ forwardRef<
+ HTMLDivElement,
{
- content,
- side,
- asChild = true,
- children,
- styleClasses,
- delayDuration = 500,
- open,
- align,
- setOpen,
- avoidCollisions = false,
- },
- ref,
- ) => {
- if (!content) {
- return <>{children}>;
+ className?: string;
+ side?: ShadToolTipType["side"];
+ avoidCollisions?: boolean;
+ align?: ShadToolTipType["align"];
+ children: React.ReactNode;
}
+ >((props, ref) => (
+
+ {props.children}
+
+ )),
+);
+
+MemoizedTooltipContent.displayName = "MemoizedTooltipContent";
+
+// Memoize the main tooltip component
+const ShadTooltip = memo(
+ forwardRef(
+ (
+ {
+ content,
+ side,
+ asChild = true,
+ children,
+ styleClasses,
+ delayDuration = 500,
+ open,
+ align,
+ setOpen,
+ avoidCollisions = false,
+ },
+ ref,
+ ) => {
+ // Early return if no content
+ if (!content) {
+ return children;
+ }
- return (
-
- {children}
-
- {content}
-
-
- );
- },
+ // Memoize className concatenation
+ const tooltipClassName = useMemo(
+ () => cn(BASE_TOOLTIP_CLASSES, styleClasses),
+ [styleClasses],
+ );
+
+ // Memoize tooltip props
+ const tooltipProps = useMemo(
+ () => ({
+ defaultOpen: !children,
+ open,
+ onOpenChange: setOpen,
+ delayDuration,
+ }),
+ [children, open, setOpen, delayDuration],
+ );
+
+ return (
+
+ {children}
+
+ {content}
+
+
+ );
+ },
+ ),
);
+// Add display name for dev tools
ShadTooltip.displayName = "ShadTooltip";
-
export default ShadTooltip;
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse/index.tsx
new file mode 100644
index 000000000000..bdb1cf317bf3
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse/index.tsx
@@ -0,0 +1,97 @@
+import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
+import {
+ Disclosure,
+ DisclosureContent,
+ DisclosureTrigger,
+} from "@/components/ui/disclosure";
+import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
+import { APIClassType } from "@/types/api";
+import { memo, useCallback } from "react";
+import SidebarItemsList from "../sidebarItemsList";
+
+export const CategoryDisclosure = memo(function CategoryDisclosure({
+ item,
+ openCategories,
+ setOpenCategories,
+ dataFilter,
+ nodeColors,
+ chatInputAdded,
+ onDragStart,
+ sensitiveSort,
+}: {
+ item: any;
+ openCategories: string[];
+ setOpenCategories;
+ dataFilter: any;
+ nodeColors: any;
+ chatInputAdded: boolean;
+ onDragStart: (
+ event: React.DragEvent,
+ data: { type: string; node?: APIClassType },
+ ) => void;
+ sensitiveSort: (a: any, b: any) => number;
+}) {
+ const handleKeyDownInput = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ setOpenCategories((prev) =>
+ prev.includes(item.name)
+ ? prev.filter((cat) => cat !== item.name)
+ : [...prev, item.name],
+ );
+ }
+ },
+ [item.name, setOpenCategories],
+ );
+
+ return (
+ {
+ setOpenCategories((prev) =>
+ isOpen
+ ? [...prev, item.name]
+ : prev.filter((cat) => cat !== item.name),
+ );
+ }}
+ >
+
+
+
+
+
+
+ {item.display_name}
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+CategoryDisclosure.displayName = "CategoryDisclosure";
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup/index.tsx
new file mode 100644
index 000000000000..3756e37bf87c
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup/index.tsx
@@ -0,0 +1,57 @@
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+} from "@/components/ui/sidebar";
+import { memo } from "react";
+import { CategoryGroupProps } from "../../types";
+import { CategoryDisclosure } from "../categoryDisclouse";
+
+export const CategoryGroup = memo(function CategoryGroup({
+ dataFilter,
+ sortedCategories,
+ CATEGORIES,
+ openCategories,
+ setOpenCategories,
+ search,
+ nodeColors,
+ chatInputAdded,
+ onDragStart,
+ sensitiveSort,
+}: CategoryGroupProps) {
+ return (
+
+
+
+ {CATEGORIES.toSorted(
+ (a, b) =>
+ (search !== "" ? sortedCategories : CATEGORIES).findIndex(
+ (value) => value === a.name,
+ ) -
+ (search !== "" ? sortedCategories : CATEGORIES).findIndex(
+ (value) => value === b.name,
+ ),
+ ).map(
+ (item) =>
+ dataFilter[item.name] &&
+ Object.keys(dataFilter[item.name]).length > 0 && (
+
+ ),
+ )}
+
+
+
+ );
+});
+
+CategoryGroup.displayName = "CategoryGroup";
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput/index.tsx
new file mode 100644
index 000000000000..b657205aca8b
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput/index.tsx
@@ -0,0 +1,50 @@
+import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
+import { Input } from "@/components/ui/input";
+import { memo } from "react";
+import ShortcutDisplay from "../../../nodeToolbarComponent/shortcutDisplay";
+
+export const SearchInput = memo(function SearchInput({
+ searchInputRef,
+ isInputFocused,
+ search,
+ handleInputFocus,
+ handleInputBlur,
+ handleInputChange,
+}: {
+ searchInputRef: React.RefObject;
+ isInputFocused: boolean;
+ search: string;
+ handleInputFocus: (event: React.FocusEvent) => void;
+ handleInputBlur: (event: React.FocusEvent) => void;
+ handleInputChange: (event: React.ChangeEvent) => void;
+}) {
+ return (
+
+
+
+ {!isInputFocused && search === "" && (
+
+ Search{" "}
+
+
+
+
+ )}
+
+ );
+});
+
+SearchInput.displayName = "SearchInput";
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader/index.tsx
new file mode 100644
index 000000000000..448085d89722
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader/index.tsx
@@ -0,0 +1,92 @@
+import {
+ Disclosure,
+ DisclosureContent,
+ DisclosureTrigger,
+} from "@/components/ui/disclosure";
+
+import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
+import ShadTooltip from "@/components/common/shadTooltipComponent";
+import { Button } from "@/components/ui/button";
+import { SidebarHeader, SidebarTrigger } from "@/components/ui/sidebar";
+import { memo } from "react";
+import { SidebarFilterComponent } from "../../../extraSidebarComponent/sidebarFilterComponent";
+import { SidebarHeaderComponentProps } from "../../types";
+import FeatureToggles from "../featureTogglesComponent";
+import { SearchInput } from "../searchInput";
+
+export const SidebarHeaderComponent = memo(function SidebarHeaderComponent({
+ showConfig,
+ setShowConfig,
+ showBeta,
+ setShowBeta,
+ showLegacy,
+ setShowLegacy,
+ searchInputRef,
+ isInputFocused,
+ search,
+ handleInputFocus,
+ handleInputBlur,
+ handleInputChange,
+ filterType,
+ setFilterEdge,
+ setFilterData,
+ data,
+}: SidebarHeaderComponentProps) {
+ return (
+
+
+
+
+
+
+
Components
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {filterType && (
+ {
+ setFilterEdge([]);
+ setFilterData(data);
+ }}
+ />
+ )}
+
+ );
+});
+
+SidebarHeaderComponent.displayName = "SidebarHeaderComponent";
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
index 93ab28e15435..cf2fe3bda15d 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
@@ -1,16 +1,13 @@
import Fuse from "fuse.js";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook"; // Import useHotkeys
import ForwardedIconComponent from "@/components/common/genericIconComponent";
-import ShadTooltip from "@/components/common/shadTooltipComponent";
-import { Button } from "@/components/ui/button";
import {
Disclosure,
DisclosureContent,
DisclosureTrigger,
} from "@/components/ui/disclosure";
-import { Input } from "@/components/ui/input";
import {
Sidebar,
SidebarContent,
@@ -18,12 +15,9 @@ import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
- SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
- SidebarMenuSkeleton,
- SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import { useAddComponent } from "@/hooks/useAddComponent";
@@ -39,12 +33,11 @@ import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api";
-import { SidebarFilterComponent } from "../extraSidebarComponent/sidebarFilterComponent";
import sensitiveSort from "../extraSidebarComponent/utils/sensitive-sort";
-import ShortcutDisplay from "../nodeToolbarComponent/shortcutDisplay";
+import { CategoryGroup } from "./components/categoryGroup";
import NoResultsMessage from "./components/emptySearchComponent";
-import FeatureToggles from "./components/featureTogglesComponent";
import SidebarMenuButtons from "./components/sidebarFooterButtons";
+import { SidebarHeaderComponent } from "./components/sidebarHeader";
import SidebarItemsList from "./components/sidebarItemsList";
import { applyBetaFilter } from "./helpers/apply-beta-filter";
import { applyEdgeFilter } from "./helpers/apply-edge-filter";
@@ -58,15 +51,103 @@ const CATEGORIES = SIDEBAR_CATEGORIES;
const BUNDLES = SIDEBAR_BUNDLES;
export function FlowSidebarComponent() {
- const [isInputFocused, setIsInputFocused] = useState(false);
- const searchInputRef = useRef(null);
+ const { data, templates } = useTypesStore(
+ useCallback(
+ (state) => ({
+ data: state.data,
+ templates: state.templates,
+ }),
+ [],
+ ),
+ );
+
+ const { getFilterEdge, setFilterEdge, filterType, nodes } = useFlowStore(
+ useCallback(
+ (state) => ({
+ getFilterEdge: state.getFilterEdge,
+ setFilterEdge: state.setFilterEdge,
+ filterType: state.filterType,
+ nodes: state.nodes,
+ }),
+ [],
+ ),
+ );
- const data = useTypesStore((state) => state.data);
- const templates = useTypesStore((state) => state.templates);
- const getFilterEdge = useFlowStore((state) => state.getFilterEdge);
- const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const hasStore = useStoreStore((state) => state.hasStore);
- const filterType = useFlowStore((state) => state.filterType);
+
+ // Memoized values
+ const chatInputAdded = useMemo(() => checkChatInput(nodes), [nodes]);
+
+ const customComponent = useMemo(() => {
+ return data?.["custom_component"]?.["CustomComponent"] ?? null;
+ }, [data]);
+
+ const getFilteredData = useCallback(
+ (searchTerm: string, sourceData: any, fuseInstance: Fuse | null) => {
+ if (!searchTerm) return sourceData;
+
+ let filteredData = cloneDeep(sourceData);
+ // ... rest of your filtering logic
+ return filteredData;
+ },
+ [],
+ );
+
+ // Effect optimizations
+ useEffect(() => {
+ if (filterType) {
+ setOpen(true);
+ }
+ }, [filterType]);
+
+ useEffect(() => {
+ const fuseOptions = {
+ keys: ["display_name", "description", "type", "category"],
+ threshold: 0.2,
+ includeScore: true,
+ };
+
+ const fuseData = Object.entries(data).flatMap(([category, items]) =>
+ Object.entries(items).map(([key, value]) => ({
+ ...value,
+ category,
+ key,
+ })),
+ );
+
+ setFuse(new Fuse(fuseData, fuseOptions));
+ }, [data]);
+
+ // Event handlers
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
+ if (event.key === "/") {
+ event.preventDefault();
+ searchInputRef.current?.focus();
+ setOpen(true);
+ }
+ }, []);
+
+ const handleKeyDownInput = (
+ e: React.KeyboardEvent,
+ name: string,
+ ) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ setOpenCategories((prev) =>
+ prev.includes(name)
+ ? prev.filter((cat) => cat !== name)
+ : [...prev, name],
+ );
+ }
+ };
+
+ useEffect(() => {
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [handleKeyDown]);
+
+ const [isInputFocused, setIsInputFocused] = useState(false);
+ const searchInputRef = useRef(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [dataFilter, setFilterData] = useState(data);
@@ -231,10 +312,14 @@ export function FlowSidebarComponent() {
}
};
- function handleSearchInput(e: string) {
- setSearch(e);
- filterComponents();
- }
+ const handleSearchInput = useCallback(
+ (value: string) => {
+ setSearch(value);
+ const filtered = getFilteredData(value, data, fuse);
+ setFilterData(filtered);
+ },
+ [data, fuse],
+ );
function onDragStart(
event: React.DragEvent,
@@ -252,24 +337,6 @@ export function FlowSidebarComponent() {
event.dataTransfer.setData("genericNode", JSON.stringify(data));
}
- const customComponent = useMemo(() => {
- return data?.["custom_component"]?.["CustomComponent"] ?? null;
- }, [data]);
-
- const handleKeyDown = (
- e: React.KeyboardEvent,
- name: string,
- ) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- setOpenCategories((prev) =>
- prev.includes(name)
- ? prev.filter((cat) => cat !== name)
- : [...prev, name],
- );
- }
- };
-
const hasBundleItems = BUNDLES.some(
(item) =>
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
@@ -286,9 +353,6 @@ export function FlowSidebarComponent() {
setOpenCategories([]);
}
- const nodes = useFlowStore((state) => state.nodes);
- const chatInputAdded = checkChatInput(nodes);
-
const handleInputFocus = useCallback(
(event: React.FocusEvent) => {
setIsInputFocused(true);
@@ -316,156 +380,40 @@ export function FlowSidebarComponent() {
data-testid="shad-sidebar"
className="noflow"
>
-
-
-
-
-
-
-
Components
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {!isInputFocused && search === "" && (
-
- Search{" "}
-
-
-
-
- )}
-
- {filterType && (
- {
- setFilterEdge([]);
- setFilterData(data);
- }}
- />
- )}
-
+
{hasResults ? (
<>
{hasCategoryItems && (
-
-
-
- {!data
- ? Array.from({ length: 5 }).map((_, index) => (
-
-
-
- ))
- : CATEGORIES.toSorted(
- (a, b) =>
- (search !== ""
- ? sortedCategories
- : CATEGORIES
- ).findIndex((value) => value === a.name) -
- (search !== ""
- ? sortedCategories
- : CATEGORIES
- ).findIndex((value) => value === b.name),
- ).map(
- (item) =>
- dataFilter[item.name] &&
- Object.keys(dataFilter[item.name]).length > 0 && (
- {
- setOpenCategories((prev) =>
- isOpen
- ? [...prev, item.name]
- : prev.filter((cat) => cat !== item.name),
- );
- }}
- >
-
-
-
-
- handleKeyDown(e, item.name)
- }
- className="flex cursor-pointer items-center gap-2"
- >
-
-
- {item.display_name}
-
-
-
-
-
-
-
-
-
-
- ),
- )}
-
-
-
+
)}
{hasBundleItems && (
@@ -501,7 +449,7 @@ export function FlowSidebarComponent() {
- handleKeyDown(e, item.name)
+ handleKeyDownInput(e, item.name)
}
className="flex cursor-pointer items-center gap-2"
data-testid={`disclosure-bundles-${item.display_name.toLocaleLowerCase()}`}
@@ -553,3 +501,7 @@ export function FlowSidebarComponent() {
);
}
+
+FlowSidebarComponent.displayName = "FlowSidebarComponent";
+
+export default memo(FlowSidebarComponent);
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/types/index.ts b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/types/index.ts
new file mode 100644
index 000000000000..8214933bcaf5
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/types/index.ts
@@ -0,0 +1,52 @@
+import { APIClassType, APIDataType } from "@/types/api";
+import { Dispatch, SetStateAction } from "react";
+
+export interface CategoryGroupProps {
+ dataFilter: APIDataType;
+ sortedCategories: string[];
+ CATEGORIES: {
+ display_name: string;
+ name: string;
+ icon: string;
+ }[];
+ openCategories: string[];
+ setOpenCategories: (categories: string[]) => void;
+ search: string;
+ nodeColors: {
+ [key: string]: string;
+ };
+ chatInputAdded: boolean;
+ onDragStart: (
+ event: React.DragEvent
,
+ data: { type: string; node?: APIClassType },
+ ) => void;
+ sensitiveSort: (a: string, b: string) => number;
+}
+
+export interface SidebarHeaderComponentProps {
+ showConfig: boolean;
+ setShowConfig: (show: boolean) => void;
+ showBeta: boolean;
+ setShowBeta: (show: boolean) => void;
+ showLegacy: boolean;
+ setShowLegacy: (show: boolean) => void;
+ searchInputRef: React.RefObject;
+ isInputFocused: boolean;
+ search: string;
+ handleInputFocus: (event: React.FocusEvent) => void;
+ handleInputBlur: (event: React.FocusEvent) => void;
+ handleInputChange: (event: React.ChangeEvent) => void;
+ filterType:
+ | {
+ source: string | undefined;
+ sourceHandle: string | undefined;
+ target: string | undefined;
+ targetHandle: string | undefined;
+ type: string;
+ color: string;
+ }
+ | undefined;
+ setFilterEdge: (edge: any[]) => void;
+ setFilterData: Dispatch>;
+ data: APIDataType;
+}
diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/components/toolbar-button.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/components/toolbar-button.tsx
new file mode 100644
index 000000000000..01a41e4af528
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/components/toolbar-button.tsx
@@ -0,0 +1,35 @@
+import { Button } from "@/components/ui/button";
+import { memo } from "react";
+
+import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
+import ShadTooltip from "@/components/common/shadTooltipComponent";
+import { cn } from "@/utils/utils";
+import ShortcutDisplay from "../shortcutDisplay";
+
+export const ToolbarButton = memo(
+ ({
+ onClick,
+ icon,
+ label,
+ shortcut,
+ className,
+ }: {
+ onClick: () => void;
+ icon: string;
+ label?: string;
+ shortcut?: any;
+ className?: string;
+ }) => (
+ } side="top">
+
+
+ ),
+);
diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/components/toolbar-modals.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/components/toolbar-modals.tsx
new file mode 100644
index 000000000000..47b8bf24ade8
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/components/toolbar-modals.tsx
@@ -0,0 +1,146 @@
+import CodeAreaModal from "@/modals/codeAreaModal";
+import ConfirmationModal from "@/modals/confirmationModal";
+import EditNodeModal from "@/modals/editNodeModal";
+import ShareModal from "@/modals/shareModal";
+import { APIClassType } from "@/types/api";
+import { FlowType } from "@/types/flow";
+import React, { memo } from "react";
+
+interface ToolbarModalsProps {
+ // Modal visibility states
+ showModalAdvanced: boolean;
+ showconfirmShare: boolean;
+ showOverrideModal: boolean;
+ openModal: boolean;
+ hasCode: boolean;
+
+ // Setters for modal states
+ setShowModalAdvanced: (value: boolean) => void;
+ setShowconfirmShare: (value: boolean) => void;
+ setShowOverrideModal: (value: boolean) => void;
+ setOpenModal: (value: boolean) => void;
+
+ // Data and handlers
+ data: any;
+ flowComponent: FlowType;
+ handleOnNewValue: (value: string | string[]) => void;
+ handleNodeClass: (apiClassType: APIClassType, type: string) => void;
+ setToolMode: (value: boolean) => void;
+ setSuccessData: (data: { title: string }) => void;
+ addFlow: (params: { flow: FlowType; override: boolean }) => void;
+ name?: string;
+}
+
+const ToolbarModals = memo(
+ ({
+ showModalAdvanced,
+ showconfirmShare,
+ showOverrideModal,
+ openModal,
+ hasCode,
+ setShowModalAdvanced,
+ setShowconfirmShare,
+ setShowOverrideModal,
+ setOpenModal,
+ data,
+ flowComponent,
+ handleOnNewValue,
+ handleNodeClass,
+ setToolMode,
+ setSuccessData,
+ addFlow,
+ name = "code",
+ }: ToolbarModalsProps) => {
+ // Handlers for confirmation modal
+ const handleConfirm = () => {
+ addFlow({
+ flow: flowComponent,
+ override: true,
+ });
+ setSuccessData({ title: `${data.id} successfully overridden!` });
+ setShowOverrideModal(false);
+ };
+
+ const handleClose = () => {
+ setShowOverrideModal(false);
+ };
+
+ const handleCancel = () => {
+ addFlow({
+ flow: flowComponent,
+ override: true,
+ });
+ setSuccessData({ title: "New component successfully saved!" });
+ setShowOverrideModal(false);
+ };
+
+ return (
+ <>
+ {showModalAdvanced && (
+
+ )}
+
+ {showconfirmShare && (
+
+ )}
+
+ {showOverrideModal && (
+
+
+
+ It seems {data.node?.display_name} already exists. Do you want
+ to replace it with the current or create a new one?
+
+
+
+ )}
+
+ {hasCode && (
+
+ {openModal && (
+ {
+ handleNodeClass(apiClassType, type);
+ setToolMode(false);
+ }}
+ nodeClass={data.node}
+ value={data.node?.template[name]?.value ?? ""}
+ componentId={data.id}
+ >
+ <>>
+
+ )}
+
+ )}
+ >
+ );
+ },
+);
+
+ToolbarModals.displayName = "ToolbarModals";
+
+export default ToolbarModals;
diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx
index e698e9fde1c6..71523851dab8 100644
--- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx
@@ -3,7 +3,6 @@ import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
import ShadTooltip from "@/components/common/shadTooltipComponent";
-import ToggleShadComponent from "@/components/core/parameterRenderComponent/components/toggleShadComponent";
import { Button } from "@/components/ui/button";
import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow";
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
@@ -12,7 +11,7 @@ import useAddFlow from "@/hooks/flows/use-add-flow";
import CodeAreaModal from "@/modals/codeAreaModal";
import { APIClassType } from "@/types/api";
import _, { cloneDeep } from "lodash";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useUpdateNodeInternals } from "reactflow";
import IconComponent from "../../../../components/common/genericIconComponent";
import {
@@ -40,796 +39,730 @@ import {
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { cn, getNodeLength, openInNewTab } from "../../../../utils/utils";
+import { ToolbarButton } from "./components/toolbar-button";
+import ToolbarModals from "./components/toolbar-modals";
import useShortcuts from "./hooks/use-shortcuts";
-import ShortcutDisplay from "./shortcutDisplay";
import ToolbarSelectItem from "./toolbarSelectItem";
-export default function NodeToolbarComponent({
- data,
- deleteNode,
- setShowNode,
- numberOfOutputHandles,
- showNode,
- name = "code",
- onCloseAdvancedModal,
- updateNode,
- isOutdated,
- setOpenShowMoreOptions,
-}: nodeToolbarPropsType): JSX.Element {
- const version = useDarkStore((state) => state.version);
- const [showModalAdvanced, setShowModalAdvanced] = useState(false);
- const [showconfirmShare, setShowconfirmShare] = useState(false);
- const [showOverrideModal, setShowOverrideModal] = useState(false);
- const [flowComponent, setFlowComponent] = useState(
- createFlowComponent(cloneDeep(data), version),
- );
- const nodeLength = getNodeLength(data);
- const updateFreezeStatus = useFlowStore((state) => state.updateFreezeStatus);
- const hasStore = useStoreStore((state) => state.hasStore);
- const hasApiKey = useStoreStore((state) => state.hasApiKey);
- const validApiKey = useStoreStore((state) => state.validApiKey);
- const shortcuts = useShortcutsStore((state) => state.shortcuts);
- const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
- const [openModal, setOpenModal] = useState(false);
- const isGroup = data.node?.flow ? true : false;
- const frozen = data.node?.frozen ?? false;
- const currentFlow = useFlowStore((state) => state.currentFlow);
-
- const addFlow = useAddFlow();
-
- const { mutate: patchUpdateFlow } = usePatchUpdateFlow();
-
- const isMinimal = countHandlesFn(data) <= 1 && numberOfOutputHandles <= 1;
- function activateToolMode() {
- const newValue = !toolMode;
- setToolMode(newValue);
-
- updateToolMode(data.id, newValue);
- data.node!.tool_mode = newValue;
-
- mutateTemplate(
- newValue,
- data.node!,
- handleNodeClass,
- postToolModeValue,
- setErrorData,
- "tool_mode",
- () => {
- const node = currentFlow?.data?.nodes.find(
- (node) => node.id === data.id,
- );
- const index = currentFlow?.data?.nodes.indexOf(node!)!;
- currentFlow!.data!.nodes[index]!.data.node.tool_mode = newValue;
-
- patchUpdateFlow({
- id: currentFlow?.id!,
- name: currentFlow?.name!,
- data: currentFlow?.data!,
- description: currentFlow?.description!,
- folder_id: currentFlow?.folder_id!,
- endpoint_name: currentFlow?.endpoint_name!,
- });
- },
+const NodeToolbarComponent = memo(
+ ({
+ data,
+ deleteNode,
+ setShowNode,
+ numberOfOutputHandles,
+ showNode,
+ name = "code",
+ onCloseAdvancedModal,
+ updateNode,
+ isOutdated,
+ setOpenShowMoreOptions,
+ }: nodeToolbarPropsType): JSX.Element => {
+ const version = useDarkStore((state) => state.version);
+ const [showModalAdvanced, setShowModalAdvanced] = useState(false);
+ const [showconfirmShare, setShowconfirmShare] = useState(false);
+ const [showOverrideModal, setShowOverrideModal] = useState(false);
+ const [flowComponent, setFlowComponent] = useState(
+ createFlowComponent(cloneDeep(data), version),
+ );
+ const updateFreezeStatus = useFlowStore(
+ (state) => state.updateFreezeStatus,
+ );
+ const { hasStore, hasApiKey, validApiKey } = useStoreStore((state) => ({
+ hasStore: state.hasStore,
+ hasApiKey: state.hasApiKey,
+ validApiKey: state.validApiKey,
+ }));
+ const shortcuts = useShortcutsStore((state) => state.shortcuts);
+ const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
+ const [openModal, setOpenModal] = useState(false);
+ const frozen = data.node?.frozen ?? false;
+ const currentFlow = useFlowStore((state) => state.currentFlow);
+
+ const nodeLength = useMemo(() => getNodeLength(data), [data]);
+ const hasCode = useMemo(
+ () => Object.keys(data.node!.template).includes("code"),
+ [data.node],
+ );
+ // Check if any of the data.node.template fields have tool_mode as True
+ // if so we can show the tool mode button
+ const hasToolMode = useMemo(
+ () => checkHasToolMode(data.node?.template ?? {}),
+ [data.node?.template],
+ );
+ const isGroup = useMemo(
+ () => (data.node?.flow ? true : false),
+ [data.node],
);
- updateNodeInternals(data.id);
- }
- function minimize() {
- if (isMinimal || !showNode) {
- setShowNode((data.showNode ?? true) ? false : true);
- updateNodeInternals(data.id);
- return;
- }
- setNoticeData({
- title:
- "Minimization are only available for components with one handle or fewer.",
- });
- return;
- }
+ const addFlow = useAddFlow();
- function handleungroup() {
- if (isGroup) {
- takeSnapshot();
- expandGroupNode(
- data.id,
- updateFlowPosition(getNodePosition(data.id), data.node?.flow!),
- data.node!.template,
- nodes,
- edges,
- setNodes,
- setEdges,
- data.node?.outputs,
- );
- }
- }
+ const { mutate: patchUpdateFlow } = usePatchUpdateFlow();
- function shareComponent() {
- if (hasApiKey || hasStore) {
- setShowconfirmShare((state) => !state);
- }
- }
+ const isMinimal = useMemo(
+ () => countHandlesFn(data) <= 1 && numberOfOutputHandles <= 1,
+ [data, numberOfOutputHandles],
+ );
- function handleCodeModal() {
- if (!hasCode)
- setNoticeData({ title: `You can not access ${data.id} code` });
- setOpenModal((state) => !state);
- }
+ const [toolMode, setToolMode] = useState(() => {
+ // Check if tool mode is explicitly set on the node
+ const hasToolModeProperty = data.node?.tool_mode;
+ if (hasToolModeProperty) {
+ return hasToolModeProperty;
+ }
- function saveComponent() {
- if (isSaved) {
- setShowOverrideModal((state) => !state);
- return;
- }
- addFlow({
- flow: flowComponent,
- override: false,
- });
- setSuccessData({ title: `${data.id} saved successfully` });
- return;
- }
- // Check if any of the data.node.template fields have tool_mode as True
- // if so we can show the tool mode button
- const hasToolMode = checkHasToolMode(data.node?.template ?? {});
-
- function openDocs() {
- if (data.node?.documentation) {
- return openInNewTab(data.node?.documentation);
- }
- setNoticeData({
- title: `${data.id} docs is not available at the moment.`,
- });
- }
-
- const freezeFunction = () => {
- setNode(data.id, (old) => ({
- ...old,
- data: {
- ...old.data,
- node: {
- ...old.data.node,
- frozen: old.data?.node?.frozen ? false : true,
- },
- },
- }));
- };
-
- useShortcuts({
- showOverrideModal,
- showModalAdvanced,
- openModal,
- showconfirmShare,
- FreezeAllVertices: () => {
- FreezeAllVertices({ flowId: currentFlowId, stopNodeId: data.id });
- },
- Freeze: freezeFunction,
- downloadFunction: () => downloadNode(flowComponent!),
- displayDocs: openDocs,
- saveComponent,
- showAdvance: () => setShowModalAdvanced((state) => !state),
- handleCodeModal,
- shareComponent,
- ungroup: handleungroup,
- minimizeFunction: minimize,
- activateToolMode: activateToolMode,
- hasToolMode,
- });
-
- const paste = useFlowStore((state) => state.paste);
- const nodes = useFlowStore((state) => state.nodes);
- const edges = useFlowStore((state) => state.edges);
- const setNodes = useFlowStore((state) => state.setNodes);
- const setEdges = useFlowStore((state) => state.setEdges);
- const getNodePosition = useFlowStore((state) => state.getNodePosition);
- const flows = useFlowsManagerStore((state) => state.flows);
- const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
- const { mutate: FreezeAllVertices } = usePostRetrieveVertexOrder({
- onSuccess: ({ vertices_to_run }) => {
- updateFreezeStatus(vertices_to_run, !data.node?.frozen);
- vertices_to_run.forEach((vertex) => {
- updateNodeInternals(vertex);
- });
- },
- });
- const updateToolMode = useFlowStore((state) => state.updateToolMode);
+ // Otherwise check if node has component_as_tool output
+ const hasComponentAsTool = data.node?.outputs?.some(
+ (output) => output.name === "component_as_tool",
+ );
- useEffect(() => {
- if (!showModalAdvanced) {
- onCloseAdvancedModal!(false);
- }
- }, [showModalAdvanced]);
- const updateNodeInternals = useUpdateNodeInternals();
+ return hasComponentAsTool ?? false;
+ });
- const setLastCopiedSelection = useFlowStore(
- (state) => state.setLastCopiedSelection,
- );
+ const handleActivateToolMode = useCallback(() => {
+ const newValue = !toolMode;
+ updateToolMode(data.id, newValue);
+ data.node!.tool_mode = newValue;
+
+ mutateTemplate(
+ newValue,
+ data.node!,
+ handleNodeClass,
+ postToolModeValue,
+ setErrorData,
+ "tool_mode",
+ () => {
+ const node = currentFlow?.data?.nodes.find((n) => n.id === data.id);
+ const index = currentFlow?.data?.nodes.indexOf(node!)!;
+ currentFlow!.data!.nodes[index]!.data.node.tool_mode = newValue;
+
+ patchUpdateFlow({
+ id: currentFlow?.id!,
+ name: currentFlow?.name!,
+ data: currentFlow?.data!,
+ description: currentFlow?.description!,
+ folder_id: currentFlow?.folder_id!,
+ endpoint_name: currentFlow?.endpoint_name!,
+ });
+ },
+ );
- const setSuccessData = useAlertStore((state) => state.setSuccessData);
- const setNoticeData = useAlertStore((state) => state.setNoticeData);
- const setErrorData = useAlertStore((state) => state.setErrorData);
+ updateNodeInternals(data.id);
+ }, [toolMode, data, currentFlow]);
+
+ const handleMinimize = useCallback(() => {
+ if (isMinimal || !showNode) {
+ setShowNode(!showNode);
+ updateNodeInternals(data.id);
+ return;
+ }
+ setNoticeData({
+ title:
+ "Minimization only available for components with one handle or fewer.",
+ });
+ }, [isMinimal, showNode, data.id]);
- useEffect(() => {
- setFlowComponent(createFlowComponent(cloneDeep(data), version));
- }, [
- data,
- data.node,
- data.node?.display_name,
- data.node?.description,
- data.node?.template,
- showModalAdvanced,
- showconfirmShare,
- ]);
-
- const [selectedValue, setSelectedValue] = useState(null);
-
- const handleSelectChange = (event) => {
- setSelectedValue(event);
-
- switch (event) {
- case "save":
- saveComponent();
- break;
- case "freeze":
- freezeFunction();
- break;
- case "freezeAll":
- FreezeAllVertices({ flowId: currentFlowId, stopNodeId: data.id });
- break;
- case "code":
- setOpenModal(!openModal);
- break;
- case "advanced":
- setShowModalAdvanced(true);
- break;
- case "show":
+ function handleungroup() {
+ if (isGroup) {
takeSnapshot();
- minimize();
- break;
- case "Share":
- shareComponent();
- break;
- case "Download":
- downloadNode(flowComponent!);
- break;
- case "SaveAll":
- addFlow({
- flow: flowComponent,
- override: false,
- });
- break;
- case "documentation":
- openDocs();
- break;
- case "disabled":
- break;
- case "ungroup":
- handleungroup();
- break;
- case "override":
- setShowOverrideModal(true);
- break;
- case "delete":
- deleteNode(data.id);
- break;
- case "update":
- updateNode();
- break;
- case "copy":
- const node = nodes.filter((node) => node.id === data.id);
- setLastCopiedSelection({ nodes: _.cloneDeep(node), edges: [] });
- break;
- case "duplicate":
- paste(
- {
- nodes: [nodes.find((node) => node.id === data.id)!],
- edges: [],
- },
- {
- x: 50,
- y: 10,
- paneX: nodes.find((node) => node.id === data.id)?.position.x,
- paneY: nodes.find((node) => node.id === data.id)?.position.y,
- },
+ expandGroupNode(
+ data.id,
+ updateFlowPosition(getNodePosition(data.id), data.node?.flow!),
+ data.node!.template,
+ nodes,
+ edges,
+ setNodes,
+ setEdges,
+ data.node?.outputs,
);
- break;
- case "toolMode":
- activateToolMode();
- break;
+ }
}
- setSelectedValue(null);
- };
+ function shareComponent() {
+ if (hasApiKey || hasStore) {
+ setShowconfirmShare((state) => !state);
+ }
+ }
- const isSaved = flows?.some((flow) =>
- Object.values(flow).includes(data.node?.display_name!),
- );
+ function handleCodeModal() {
+ if (!hasCode)
+ setNoticeData({ title: `You can not access ${data.id} code` });
+ setOpenModal((state) => !state);
+ }
- const setNode = useFlowStore((state) => state.setNode);
+ function saveComponent() {
+ if (isSaved) {
+ setShowOverrideModal((state) => !state);
+ return;
+ }
+ addFlow({
+ flow: flowComponent,
+ override: false,
+ });
+ setSuccessData({ title: `${data.id} saved successfully` });
+ return;
+ }
- const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue({
- node: data.node!,
- nodeId: data.id,
- name,
- });
+ function openDocs() {
+ if (data.node?.documentation) {
+ return openInNewTab(data.node?.documentation);
+ }
+ setNoticeData({
+ title: `${data.id} docs is not available at the moment.`,
+ });
+ }
- const handleOnNewValue = (value: string | string[]) => {
- handleOnNewValueHook({ value });
- };
+ const freezeFunction = () => {
+ setNode(data.id, (old) => ({
+ ...old,
+ data: {
+ ...old.data,
+ node: {
+ ...old.data.node,
+ frozen: old.data?.node?.frozen ? false : true,
+ },
+ },
+ }));
+ };
+
+ useShortcuts({
+ showOverrideModal,
+ showModalAdvanced,
+ openModal,
+ showconfirmShare,
+ FreezeAllVertices: () => {
+ FreezeAllVertices({ flowId: currentFlowId, stopNodeId: data.id });
+ },
+ Freeze: freezeFunction,
+ downloadFunction: () => downloadNode(flowComponent!),
+ displayDocs: openDocs,
+ saveComponent,
+ showAdvance: () => setShowModalAdvanced((state) => !state),
+ handleCodeModal,
+ shareComponent,
+ ungroup: handleungroup,
+ minimizeFunction: handleMinimize,
+ activateToolMode: handleActivateToolMode,
+ hasToolMode,
+ });
- const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(data.id);
+ const paste = useFlowStore((state) => state.paste);
+ const nodes = useFlowStore((state) => state.nodes);
+ const edges = useFlowStore((state) => state.edges);
+ const setNodes = useFlowStore((state) => state.setNodes);
+ const setEdges = useFlowStore((state) => state.setEdges);
+ const getNodePosition = useFlowStore((state) => state.getNodePosition);
+ const flows = useFlowsManagerStore((state) => state.flows);
+ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
+ const { mutate: FreezeAllVertices } = usePostRetrieveVertexOrder({
+ onSuccess: ({ vertices_to_run }) => {
+ updateFreezeStatus(vertices_to_run, !data.node?.frozen);
+ vertices_to_run.forEach((vertex) => {
+ updateNodeInternals(vertex);
+ });
+ },
+ });
+ const updateToolMode = useFlowStore((state) => state.updateToolMode);
- const handleNodeClass = (newNodeClass: APIClassType, type: string) => {
- handleNodeClassHook(newNodeClass, type);
- };
+ useEffect(() => {
+ if (!showModalAdvanced) {
+ onCloseAdvancedModal!(false);
+ }
+ }, [showModalAdvanced]);
+ const updateNodeInternals = useUpdateNodeInternals();
- const hasCode = Object.keys(data.node!.template).includes("code");
+ const setLastCopiedSelection = useFlowStore(
+ (state) => state.setLastCopiedSelection,
+ );
- const selectTriggerRef = useRef(null);
+ const setSuccessData = useAlertStore((state) => state.setSuccessData);
+ const setNoticeData = useAlertStore((state) => state.setNoticeData);
+ const setErrorData = useAlertStore((state) => state.setErrorData);
+
+ useEffect(() => {
+ setFlowComponent(createFlowComponent(cloneDeep(data), version));
+ }, [
+ data,
+ data.node,
+ data.node?.display_name,
+ data.node?.description,
+ data.node?.template,
+ showModalAdvanced,
+ showconfirmShare,
+ ]);
+
+ const [selectedValue, setSelectedValue] = useState(null);
+
+ const handleSelectChange = useCallback((event) => {
+ setSelectedValue(event);
+
+ switch (event) {
+ case "save":
+ saveComponent();
+ break;
+ case "freeze":
+ freezeFunction();
+ break;
+ case "freezeAll":
+ FreezeAllVertices({ flowId: currentFlowId, stopNodeId: data.id });
+ break;
+ case "code":
+ setOpenModal(!openModal);
+ break;
+ case "advanced":
+ setShowModalAdvanced(true);
+ break;
+ case "show":
+ takeSnapshot();
+ handleMinimize();
+ break;
+ case "Share":
+ shareComponent();
+ break;
+ case "Download":
+ downloadNode(flowComponent!);
+ break;
+ case "SaveAll":
+ addFlow({
+ flow: flowComponent,
+ override: false,
+ });
+ break;
+ case "documentation":
+ openDocs();
+ break;
+ case "disabled":
+ break;
+ case "ungroup":
+ handleungroup();
+ break;
+ case "override":
+ setShowOverrideModal(true);
+ break;
+ case "delete":
+ deleteNode(data.id);
+ break;
+ case "update":
+ updateNode();
+ break;
+ case "copy":
+ const node = nodes.filter((node) => node.id === data.id);
+ setLastCopiedSelection({ nodes: _.cloneDeep(node), edges: [] });
+ break;
+ case "duplicate":
+ paste(
+ {
+ nodes: [nodes.find((node) => node.id === data.id)!],
+ edges: [],
+ },
+ {
+ x: 50,
+ y: 10,
+ paneX: nodes.find((node) => node.id === data.id)?.position.x,
+ paneY: nodes.find((node) => node.id === data.id)?.position.y,
+ },
+ );
+ break;
+ case "toolMode":
+ handleActivateToolMode();
+ break;
+ }
+
+ setSelectedValue(null);
+ }, []);
+
+ const isSaved = flows?.some((flow) =>
+ Object.values(flow).includes(data.node?.display_name!),
+ );
- const handleButtonClick = () => {
- (selectTriggerRef.current! as HTMLElement)?.click();
- };
+ const setNode = useFlowStore((state) => state.setNode);
- const handleOpenChange = (open: boolean) => {
- setOpenShowMoreOptions && setOpenShowMoreOptions(open);
- };
+ const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue({
+ node: data.node!,
+ nodeId: data.id,
+ name,
+ });
- const [toolMode, setToolMode] = useState(() => {
- // Check if tool mode is explicitly set on the node
- const hasToolModeProperty = data.node?.tool_mode;
- if (hasToolModeProperty) {
- return hasToolModeProperty;
- }
+ const handleOnNewValue = (value: string | string[]) => {
+ handleOnNewValueHook({ value });
+ };
- // Otherwise check if node has component_as_tool output
- const hasComponentAsTool = data.node?.outputs?.some(
- (output) => output.name === "component_as_tool",
+ const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
+ data.id,
);
- return hasComponentAsTool ?? false;
- });
+ const handleNodeClass = (newNodeClass: APIClassType, type: string) => {
+ handleNodeClassHook(newNodeClass, type);
+ };
- const postToolModeValue = usePostTemplateValue({
- node: data.node!,
- nodeId: data.id,
- parameterId: "tool_mode",
- tool_mode: data.node!.tool_mode ?? false,
- });
+ const selectTriggerRef = useRef(null);
- const handleConfirm = useCallback(() => {
- addFlow({
- flow: flowComponent,
- override: true,
- });
- setSuccessData({ title: `${data.id} successfully overridden!` });
- setShowOverrideModal(false);
- }, [flowComponent, setSuccessData, setShowOverrideModal]);
-
- const handleClose = useCallback(() => {
- setShowOverrideModal(false);
- }, []);
-
- const handleCancel = useCallback(() => {
- addFlow({
- flow: flowComponent,
- override: true,
+ const handleButtonClick = () => {
+ (selectTriggerRef.current! as HTMLElement)?.click();
+ };
+
+ const handleOpenChange = (open: boolean) => {
+ setOpenShowMoreOptions && setOpenShowMoreOptions(open);
+ };
+
+ const postToolModeValue = usePostTemplateValue({
+ node: data.node!,
+ nodeId: data.id,
+ parameterId: "tool_mode",
+ tool_mode: data.node!.tool_mode ?? false,
});
- setSuccessData({ title: "New component successfully saved!" });
- setShowOverrideModal(false);
- }, [flowComponent, setSuccessData, setShowOverrideModal]);
-
- return (
- <>
-
-
- {hasCode && (
-
name.split(" ")[0].toLowerCase() === "code",
- )!}
- />
- }
- side="top"
- >
-
-
- )}
+ const handleConfirm = useCallback(() => {
+ addFlow({
+ flow: flowComponent,
+ override: true,
+ });
+ setSuccessData({ title: `${data.id} successfully overridden!` });
+ setShowOverrideModal(false);
+ }, [flowComponent, data.id]);
+
+ const handleClose = useCallback(() => {
+ setShowOverrideModal(false);
+ }, []);
+
+ const handleCancel = useCallback(() => {
+ addFlow({
+ flow: flowComponent,
+ override: true,
+ });
+ setSuccessData({ title: "New component successfully saved!" });
+ setShowOverrideModal(false);
+ }, [flowComponent, setSuccessData, setShowOverrideModal]);
+ const renderToolbarButtons = useMemo(
+ () => (
+ <>
+ {hasCode && (
+
setOpenModal(true)}
+ shortcut={shortcuts.find((s) =>
+ s.name.toLowerCase().startsWith("code"),
+ )}
+ />
+ )}
{nodeLength > 0 && (
-
- name.split(" ")[0].toLowerCase() === "advanced",
- )!}
- />
- }
- side="top"
- >
-
-
+ setShowModalAdvanced(true)}
+ shortcut={shortcuts.find((s) =>
+ s.name.toLowerCase().startsWith("advanced"),
+ )}
+ />
)}
{!hasToolMode && (
- name.toLowerCase() === "freeze path",
- )!}
- />
- }
- side="top"
- >
-
-
+ {
+ takeSnapshot();
+ FreezeAllVertices({
+ flowId: currentFlowId,
+ stopNodeId: data.id,
+ });
+ }}
+ shortcut={shortcuts.find((s) =>
+ s.name.toLowerCase().startsWith("freeze path"),
+ )}
+ className={cn("node-toolbar-buttons", frozen && "text-blue-500")}
+ />
)}
{hasToolMode && (
- name.toLowerCase() === "tool mode",
- )!}
- />
- }
- side="top"
- >
-
-
+ {
+ takeSnapshot();
+ handleSelectChange("toolMode");
+ }}
+ shortcut={shortcuts.find((s) =>
+ s.name.toLowerCase().startsWith("tool mode"),
+ )}
+ className={cn(
+ "node-toolbar-buttons h-[2rem]",
+ toolMode && "text-primary",
+ )}
+ />
)}
+ >
+ ),
+ [
+ hasCode,
+ nodeLength,
+ hasToolMode,
+ toolMode,
+ data.id,
+ takeSnapshot,
+ FreezeAllVertices,
+ currentFlowId,
+ shortcuts,
+ frozen,
+ handleSelectChange,
+ ],
+ );
-
-
-
-
-
-
-
-
-
+
+
+ {renderToolbarButtons}
+
- {hasCode && (
-
+
+
+
+
+
+
+
+
+ {hasCode && (
+
+ obj.name === "Code")?.shortcut!
+ }
+ value={"Code"}
+ icon={"Code"}
+ dataTestId="code-button-modal"
+ />
+
+ )}
+ {nodeLength > 0 && (
+
+ obj.name === "Advanced Settings",
+ )?.shortcut!
+ }
+ value={"Controls"}
+ icon={"SlidersHorizontal"}
+ dataTestId="advanced-button-modal"
+ />
+
+ )}
+
obj.name === "Code")?.shortcut!
+ shortcuts.find((obj) => obj.name === "Save Component")
+ ?.shortcut!
}
- value={"Code"}
- icon={"Code"}
- dataTestId="code-button-modal"
+ value={"Save"}
+ icon={"SaveAll"}
+ dataTestId="save-button-modal"
/>
- )}
- {nodeLength > 0 && (
-
+
obj.name === "Advanced Settings")
+ shortcuts.find((obj) => obj.name === "Duplicate")
?.shortcut!
}
- value={"Controls"}
- icon={"SlidersHorizontal"}
- dataTestId="advanced-button-modal"
+ value={"Duplicate"}
+ icon={"Copy"}
+ dataTestId="copy-button-modal"
/>
- )}
-
- obj.name === "Save Component")
- ?.shortcut!
- }
- value={"Save"}
- icon={"SaveAll"}
- dataTestId="save-button-modal"
- />
-
-
- obj.name === "Duplicate")?.shortcut!
- }
- value={"Duplicate"}
- icon={"Copy"}
- dataTestId="copy-button-modal"
- />
-
-
- obj.name === "Copy")?.shortcut!
- }
- value={"Copy"}
- icon={"Clipboard"}
- dataTestId="copy-button-modal"
- />
-
- {isOutdated && (
-
+
obj.name === "Update")?.shortcut!
+ shortcuts.find((obj) => obj.name === "Copy")?.shortcut!
}
- value={"Restore"}
- icon={"RefreshCcwDot"}
- dataTestId="update-button-modal"
+ value={"Copy"}
+ icon={"Clipboard"}
+ dataTestId="copy-button-modal"
/>
- )}
- {hasStore && (
+ {isOutdated && (
+
+ obj.name === "Update")
+ ?.shortcut!
+ }
+ value={"Restore"}
+ icon={"RefreshCcwDot"}
+ dataTestId="update-button-modal"
+ />
+
+ )}
+ {hasStore && (
+
+ obj.name === "Component Share")
+ ?.shortcut!
+ }
+ value={"Share"}
+ icon={"Share3"}
+ dataTestId="share-button-modal"
+ />
+
+ )}
+
obj.name === "Component Share")
- ?.shortcut!
+ shortcuts.find((obj) => obj.name === "Docs")?.shortcut!
}
- value={"Share"}
- icon={"Share3"}
- dataTestId="share-button-modal"
+ value={"Docs"}
+ icon={"FileText"}
+ dataTestId="docs-button-modal"
/>
- )}
-
-
- obj.name === "Docs")?.shortcut!
- }
- value={"Docs"}
- icon={"FileText"}
- dataTestId="docs-button-modal"
- />
-
- {(isMinimal || !showNode) && (
-
+ {(isMinimal || !showNode) && (
+
+ obj.name === "Minimize")
+ ?.shortcut!
+ }
+ value={showNode ? "Minimize" : "Expand"}
+ icon={showNode ? "Minimize2" : "Maximize2"}
+ dataTestId="minimize-button-modal"
+ />
+
+ )}
+ {isGroup && (
+
+ obj.name === "Group")?.shortcut!
+ }
+ value={"Ungroup"}
+ icon={"Ungroup"}
+ dataTestId="group-button-modal"
+ />
+
+ )}
+
obj.name === "Minimize")
- ?.shortcut!
+ shortcuts.find((obj) => obj.name === "Freeze")?.shortcut!
}
- value={showNode ? "Minimize" : "Expand"}
- icon={showNode ? "Minimize2" : "Maximize2"}
- dataTestId="minimize-button-modal"
+ value={"Freeze"}
+ icon={"Snowflake"}
+ dataTestId="freeze-button"
+ style={`${frozen ? " text-ice" : ""} transition-all`}
/>
- )}
- {isGroup && (
-
+
obj.name === "Group")?.shortcut!
+ shortcuts.find((obj) => obj.name === "Freeze Path")
+ ?.shortcut!
}
- value={"Ungroup"}
- icon={"Ungroup"}
- dataTestId="group-button-modal"
+ value={"Freeze Path"}
+ icon={"FreezeAll"}
+ dataTestId="freeze-path-button"
+ style={`${frozen ? " text-ice" : ""} transition-all`}
/>
- )}
-
- obj.name === "Freeze")?.shortcut!
- }
- value={"Freeze"}
- icon={"Snowflake"}
- dataTestId="freeze-button"
- style={`${frozen ? " text-ice" : ""} transition-all`}
- />
-
-
- obj.name === "Freeze Path")
- ?.shortcut!
- }
- value={"Freeze Path"}
- icon={"FreezeAll"}
- dataTestId="freeze-path-button"
- style={`${frozen ? " text-ice" : ""} transition-all`}
- />
-
-
- obj.name === "Download")?.shortcut!
- }
- value={"Download"}
- icon={"Download"}
- dataTestId="download-button-modal"
- />
-
-
-
- {" "}
- Delete{" "}
-
-
-
-
-
- {hasToolMode && (
-
+
obj.name === "Tool Mode")
+ shortcuts.find((obj) => obj.name === "Download")
?.shortcut!
}
- value={"Tool Mode"}
- icon={"Hammer"}
- dataTestId="tool-mode-button"
- style={`${toolMode ? "text-primary" : ""} transition-all`}
+ value={"Download"}
+ icon={"Download"}
+ dataTestId="download-button-modal"
/>
- )}
-
-
-
+
+
+ {" "}
+ Delete{" "}
+
+
+
+
+
+ {hasToolMode && (
+
+ obj.name === "Tool Mode")
+ ?.shortcut!
+ }
+ value={"Tool Mode"}
+ icon={"Hammer"}
+ dataTestId="tool-mode-button"
+ style={`${toolMode ? "text-primary" : ""} transition-all`}
+ />
+
+ )}
+
+
+
-
-
-
- It seems {data.node?.display_name} already exists. Do you want to
- replace it with the current or create a new one?
-
-
-
- {showModalAdvanced && (
-
- )}
- {showconfirmShare && (
-
- )}
- {hasCode && (
-
- {openModal && (
- {
- handleNodeClass(apiClassType, type);
- setToolMode(false);
- }}
- nodeClass={data.node}
- value={data.node?.template[name].value ?? ""}
- componentId={data.id}
- >
- <>>
-
- )}
-
- )}
-
- >
- );
-}
+
+ >
+ );
+ },
+);
+
+NodeToolbarComponent.displayName = "NodeToolbarComponent";
+
+export default NodeToolbarComponent;