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 ( - - ); - })} -
-
- - + + +
+
{" "} - Delete{" "} - - - + name="MoreHorizontal" + className="relative left-2 h-4 w-4" + />
- - - - -
- +
+
+ + + +
+
); -} +}); + +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, + ], + ); - - {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;