perf: Optimize component with memoization and selective store subscriptions (#5296)
* ✨ (NodeOutputfield/index.tsx): Introduce memoization for IconComponent, Button components, and OutputComponent for performance optimization 🔧 (NodeOutputfield/index.tsx): Change the import of 'useEffect' to 'useCallback' for better performance and to prevent unnecessary re-renders 🔧 (NodeOutputfield/index.tsx): Refactor the 'NodeOutputField' component to use useMemo for selective store subscriptions and computed values to improve performance and avoid unnecessary recalculations 🔧 (NodeOutputfield/index.tsx): Refactor the 'handleUpdateOutputHide' function to use useCallback for better performance and to prevent unnecessary re-renders 🔧 (NodeOutputfield/index.tsx): Refactor the 'useEffect' hook to include dependencies and prevent unnecessary re-renders 🔧 (NodeOutputfield/index.tsx): Refactor the 'Handle' component to use useMemo for memoization and performance optimization ✨ (NodeOutputfield/index.tsx): Refactor NodeOutputField component to improve readability and maintainability by extracting button and tooltip components into separate reusable components, and optimizing the structure of the output field rendering. 📝 (RenderInputParameters/index.tsx): Move sortToolModeFields import to the top of the file for better organization and readability. ✨ (GenericNode/index.tsx): Introduce memoization to optimize rendering performance by memoizing components and values 🔧 (GenericNode/index.tsx): Add useCallback to handleUpdateCode and handleUpdateCodeWShortcut functions for better performance and prevent unnecessary re-renders 📝 (GenericNode/index.tsx): Refactor code to use useEffect and useCallback hooks for better performance and readability 📝 (GenericNode/index.tsx): Refactor code to improve component structure and readability by extracting repeated logic into separate functions using useCallback 📝 (GenericNode/index.tsx): Refactor code to optimize rendering logic and improve maintainability by using memoization with React.memo ✨ (sort-tool-mode-field.ts): introduce a new helper function sortToolModeFields to sort fields based on tool mode status and field order array * 📝 (NodeOutputfield/index.tsx): add missing newline before ShadTooltip component for better code readability ✨ (handleRenderComponent/index.tsx): Add memoization to HandleContent component for performance optimization ♻️ (handleRenderComponent/index.tsx): Refactor HandleContent component to use useCallback and useMemo hooks for better code readability and maintainability 🔧 (handleRenderComponent/index.tsx): Refactor code to improve readability and maintainability by updating function signatures, using hooks more efficiently, and organizing code structure. ✨ (file.ts): refactor handleMouseDown function to use useCallback hook for better performance and memoization ♻️ (file.ts): refactor handleClick function to use useCallback hook for better performance and memoization ♻️ (handleRenderComponent/index.tsx): Refactor handleRenderComponent to improve code readability and maintainability by extracting callback functions into separate useCallback hooks and using memoization for validation function. * improve memo in several components * ✨ (handleRenderComponent/index.tsx): add data-testid attribute to handle element for improved testing and accessibility * ✨ (toolbar-button.tsx): add data-testid prop to ToolbarButton component for better testing capabilities 📝 (index.tsx): add data-testid attribute to various ToolbarButton components for better testability * ✨ (NodeOutputfield/index.tsx): Add onClick event handler to the InspectButton component to trigger a function when the button is clicked. * ✅ (freeze.spec.ts): add test case for clicking on "Close" button in the modal to ensure proper functionality * 📝 (nodeToolbarComponent/index.tsx): remove unnecessary dataTestId attribute from freeze-path-button to clean up code and improve readability * 🐛 (GenericNode/index.tsx): Fix potential error when outputs is null or undefined by adding optional chaining 🐛 (auto-save-off.spec.ts): Update selector for "Saved" text to target the last occurrence 🐛 (auto-save-off.spec.ts): Update selector for "Unsaved changes will be permanently lost." text to handle dynamic rendering 🐛 (auto-save-off.spec.ts): Update selector for "NVIDIA" text to ensure it is not visible 🐛 (auto-save-off.spec.ts): Update drag and drop logic for NVIDIA model to ensure correct behavior 🐛 (auto-save-off.spec.ts): Update hover logic and add component button handling for NVIDIA model to ensure correct behavior * ✨ (parameterRenderComponent/index.tsx): Refactor ParameterRenderComponent to improve performance by memoizing components and props, and using useCallback and useMemo for better optimization. 📝 (ui/disclosure.tsx): Update imports and add new React hooks for better code organization and performance 📝 (ui/disclosure.tsx): Refactor DisclosureProvider component to use useCallback and useMemo for better performance 📝 (ui/disclosure.tsx): Refactor DisclosureTrigger component to use useCallback and useMemo for better performance 📝 (ui/disclosure.tsx): Refactor DisclosureContent component to use useCallback and useMemo for better performance 📝 (ui/disclosure.tsx): Refactor Disclosure component to use memo for better performance 📝 (ui/disclosure.tsx): Refactor DisclosureTrigger component to use memo for better performance 📝 (ui/disclosure.tsx): Refactor DisclosureContent component to use memo for better performance 📝 (ui/disclosure.tsx): Refactor DisclosureProvider component to use memo for better performance 📝 (nodeToolbarComponent/index.tsx): Refactor NodeToolbarComponent to use useCallback and useMemo for better performance * ✨ (use-handle-new-value.tsx): Memoize postTemplateValue and updateNodeState functions to prevent unnecessary re-renders and improve performance 📝 (use-handle-new-value.tsx): Memoize handleOnNewValue function to optimize performance by preventing unnecessary re-renders and improve code readability --------- Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
This commit is contained in:
parent
3fdb4e87ba
commit
2a95b52e06
23 changed files with 2799 additions and 1992 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { memo, useEffect, useMemo, useRef } from "react";
|
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { useUpdateNodeInternals } from "reactflow";
|
import { useUpdateNodeInternals } from "reactflow";
|
||||||
import { default as IconComponent } from "../../../../components/common/genericIconComponent";
|
import { default as IconComponent } from "../../../../components/common/genericIconComponent";
|
||||||
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
||||||
|
|
@ -22,7 +22,117 @@ import OutputComponent from "../OutputComponent";
|
||||||
import HandleRenderComponent from "../handleRenderComponent";
|
import HandleRenderComponent from "../handleRenderComponent";
|
||||||
import OutputModal from "../outputModal";
|
import OutputModal from "../outputModal";
|
||||||
|
|
||||||
export default function NodeOutputField({
|
// Memoize IconComponent instances
|
||||||
|
const EyeIcon = memo(
|
||||||
|
({ hidden, className }: { hidden: boolean; className: string }) => (
|
||||||
|
<IconComponent
|
||||||
|
className={className}
|
||||||
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
|
name={hidden ? "EyeOff" : "Eye"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SnowflakeIcon = memo(() => (
|
||||||
|
<IconComponent className="h-5 w-5 text-ice" name="Snowflake" />
|
||||||
|
));
|
||||||
|
|
||||||
|
const ScanEyeIcon = memo(({ className }: { className: string }) => (
|
||||||
|
<IconComponent
|
||||||
|
className={className}
|
||||||
|
name="ScanEye"
|
||||||
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
// Memoize Button components
|
||||||
|
const HideShowButton = memo(
|
||||||
|
({
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
hidden,
|
||||||
|
isToolMode,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
disabled: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
hidden: boolean;
|
||||||
|
isToolMode: boolean;
|
||||||
|
title: string;
|
||||||
|
}) => (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
unstyled
|
||||||
|
onClick={onClick}
|
||||||
|
data-testid={`input-inspection-${title.toLowerCase()}`}
|
||||||
|
>
|
||||||
|
<ShadTooltip
|
||||||
|
content={disabled ? null : hidden ? "Show output" : "Hide output"}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<EyeIcon
|
||||||
|
hidden={hidden}
|
||||||
|
className={cn(
|
||||||
|
"icon-size",
|
||||||
|
disabled
|
||||||
|
? isToolMode
|
||||||
|
? "text-placeholder-foreground opacity-60"
|
||||||
|
: "text-placeholder-foreground hover:text-foreground"
|
||||||
|
: isToolMode
|
||||||
|
? "text-background hover:text-secondary-hover"
|
||||||
|
: "text-placeholder-foreground hover:text-primary-hover",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const InspectButton = memo(
|
||||||
|
({
|
||||||
|
disabled,
|
||||||
|
displayOutputPreview,
|
||||||
|
unknownOutput,
|
||||||
|
errorOutput,
|
||||||
|
isToolMode,
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
disabled: boolean | undefined;
|
||||||
|
displayOutputPreview: boolean;
|
||||||
|
unknownOutput: boolean | undefined;
|
||||||
|
errorOutput: boolean;
|
||||||
|
isToolMode: boolean;
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) => (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid={`output-inspection-${title.toLowerCase()}`}
|
||||||
|
unstyled
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<ScanEyeIcon
|
||||||
|
className={cn(
|
||||||
|
"icon-size",
|
||||||
|
isToolMode
|
||||||
|
? displayOutputPreview && !unknownOutput
|
||||||
|
? "text-background hover:text-secondary-hover"
|
||||||
|
: "cursor-not-allowed text-placeholder-foreground opacity-80"
|
||||||
|
: displayOutputPreview && !unknownOutput
|
||||||
|
? "text-foreground hover:text-primary-hover"
|
||||||
|
: "cursor-not-allowed text-placeholder-foreground opacity-60",
|
||||||
|
errorOutput ? "text-destructive" : "",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const MemoizedOutputComponent = memo(OutputComponent);
|
||||||
|
|
||||||
|
function NodeOutputField({
|
||||||
selected,
|
selected,
|
||||||
data,
|
data,
|
||||||
title,
|
title,
|
||||||
|
|
@ -39,17 +149,18 @@ export default function NodeOutputField({
|
||||||
isToolMode = false,
|
isToolMode = false,
|
||||||
}: NodeOutputFieldComponentType): JSX.Element {
|
}: NodeOutputFieldComponentType): JSX.Element {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
|
||||||
|
// Use selective store subscriptions
|
||||||
const nodes = useFlowStore((state) => state.nodes);
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
const edges = useFlowStore((state) => state.edges);
|
const edges = useFlowStore((state) => state.edges);
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
const myData = useTypesStore((state) => state.data);
|
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
|
||||||
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
|
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
|
||||||
const flowPool = useFlowStore((state) => state.flowPool);
|
const flowPool = useFlowStore((state) => state.flowPool);
|
||||||
|
const myData = useTypesStore((state) => state.data);
|
||||||
|
|
||||||
let flowPoolId = data.id;
|
// Memoize computed values
|
||||||
let internalOutputName = outputName;
|
const { flowPoolId, internalOutputName } = useMemo(() => {
|
||||||
|
|
||||||
if (data.node?.flow && outputProxy) {
|
if (data.node?.flow && outputProxy) {
|
||||||
const realOutput = getGroupOutputNodeId(
|
const realOutput = getGroupOutputNodeId(
|
||||||
data.node.flow,
|
data.node.flow,
|
||||||
|
|
@ -57,31 +168,40 @@ export default function NodeOutputField({
|
||||||
outputProxy.id,
|
outputProxy.id,
|
||||||
);
|
);
|
||||||
if (realOutput) {
|
if (realOutput) {
|
||||||
flowPoolId = realOutput.id;
|
return {
|
||||||
internalOutputName = realOutput.outputName;
|
flowPoolId: realOutput.id,
|
||||||
|
internalOutputName: realOutput.outputName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { flowPoolId: data.id, internalOutputName: outputName };
|
||||||
|
}, [data.id, data.node?.flow, outputProxy, outputName]);
|
||||||
|
|
||||||
const flowPoolNode = (flowPool[flowPoolId] ?? [])[
|
const flowPoolNode = useMemo(() => {
|
||||||
(flowPool[flowPoolId]?.length ?? 1) - 1
|
const pool = flowPool[flowPoolId] ?? [];
|
||||||
];
|
return pool[pool.length - 1];
|
||||||
|
}, [flowPool, flowPoolId]);
|
||||||
|
|
||||||
const displayOutputPreview =
|
const { displayOutputPreview, unknownOutput, errorOutput } = useMemo(
|
||||||
|
() => ({
|
||||||
|
displayOutputPreview:
|
||||||
!!flowPool[flowPoolId] &&
|
!!flowPool[flowPoolId] &&
|
||||||
logHasMessage(flowPoolNode?.data, internalOutputName);
|
logHasMessage(flowPoolNode?.data, internalOutputName),
|
||||||
|
unknownOutput: logTypeIsUnknown(flowPoolNode?.data, internalOutputName),
|
||||||
const unknownOutput = logTypeIsUnknown(
|
errorOutput: logTypeIsError(flowPoolNode?.data, internalOutputName),
|
||||||
flowPoolNode?.data,
|
}),
|
||||||
internalOutputName,
|
[flowPool, flowPoolId, flowPoolNode?.data, internalOutputName],
|
||||||
);
|
);
|
||||||
const errorOutput = logTypeIsError(flowPoolNode?.data, internalOutputName);
|
|
||||||
|
|
||||||
let disabledOutput =
|
const disabledOutput = useMemo(
|
||||||
edges.some((edge) => edge.sourceHandle === scapedJSONStringfy(id)) ?? false;
|
() => edges.some((edge) => edge.sourceHandle === scapedJSONStringfy(id)),
|
||||||
|
[edges, id],
|
||||||
|
);
|
||||||
|
|
||||||
const handleUpdateOutputHide = (value?: boolean) => {
|
const handleUpdateOutputHide = useCallback(
|
||||||
|
(value?: boolean) => {
|
||||||
setNode(data.id, (oldNode) => {
|
setNode(data.id, (oldNode) => {
|
||||||
let newNode = cloneDeep(oldNode);
|
const newNode = cloneDeep(oldNode);
|
||||||
newNode.data = {
|
newNode.data = {
|
||||||
...newNode.data,
|
...newNode.data,
|
||||||
node: {
|
node: {
|
||||||
|
|
@ -97,31 +217,19 @@ export default function NodeOutputField({
|
||||||
return newNode;
|
return newNode;
|
||||||
});
|
});
|
||||||
updateNodeInternals(data.id);
|
updateNodeInternals(data.id);
|
||||||
};
|
},
|
||||||
|
[data.id, index, setNode, updateNodeInternals],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabledOutput && data.node?.outputs![index].hidden) {
|
if (disabledOutput && data.node?.outputs![index].hidden) {
|
||||||
handleUpdateOutputHide(false);
|
handleUpdateOutputHide(false);
|
||||||
}
|
}
|
||||||
}, [disabledOutput]);
|
}, [disabledOutput, data.node?.outputs, handleUpdateOutputHide, index]);
|
||||||
|
|
||||||
const MemoizedHandleRenderComponent = memo(
|
|
||||||
HandleRenderComponent,
|
|
||||||
(prev, next) => {
|
|
||||||
return (
|
|
||||||
prev.nodeId === next.nodeId &&
|
|
||||||
prev.myData === next.myData &&
|
|
||||||
prev.showNode === next.showNode &&
|
|
||||||
prev.tooltipTitle === next.tooltipTitle &&
|
|
||||||
prev.colors === next.colors &&
|
|
||||||
prev.colorName === next.colorName
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const Handle = useMemo(
|
const Handle = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<MemoizedHandleRenderComponent
|
<HandleRenderComponent
|
||||||
left={false}
|
left={false}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
tooltipTitle={tooltipTitle}
|
tooltipTitle={tooltipTitle}
|
||||||
|
|
@ -153,9 +261,9 @@ export default function NodeOutputField({
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return !showNode ? (
|
if (!showNode) return <>{Handle}</>;
|
||||||
<>{Handle}</>
|
|
||||||
) : (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -164,52 +272,26 @@ export default function NodeOutputField({
|
||||||
isToolMode && "bg-primary",
|
isToolMode && "bg-primary",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<>
|
|
||||||
<div className="flex w-full items-center justify-end truncate text-sm">
|
<div className="flex w-full items-center justify-end truncate text-sm">
|
||||||
<div className="flex flex-1">
|
<div className="flex flex-1">
|
||||||
<Button
|
<HideShowButton
|
||||||
disabled={disabledOutput}
|
disabled={disabledOutput}
|
||||||
unstyled
|
|
||||||
onClick={() => handleUpdateOutputHide()}
|
onClick={() => handleUpdateOutputHide()}
|
||||||
data-testid={`input-inspection-${title.toLowerCase()}`}
|
hidden={!!data.node?.outputs![index].hidden}
|
||||||
>
|
isToolMode={isToolMode}
|
||||||
<ShadTooltip
|
title={title}
|
||||||
content={
|
|
||||||
disabledOutput
|
|
||||||
? null
|
|
||||||
: data.node?.outputs![index].hidden
|
|
||||||
? "Show output"
|
|
||||||
: "Hide output"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<IconComponent
|
|
||||||
className={cn(
|
|
||||||
"icon-size",
|
|
||||||
disabledOutput
|
|
||||||
? isToolMode
|
|
||||||
? "text-placeholder-foreground opacity-60"
|
|
||||||
: "text-placeholder-foreground hover:text-foreground"
|
|
||||||
: isToolMode
|
|
||||||
? "text-background hover:text-secondary-hover"
|
|
||||||
: "text-placeholder-foreground hover:text-primary-hover",
|
|
||||||
)}
|
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
|
||||||
name={data.node?.outputs![index].hidden ? "EyeOff" : "Eye"}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data.node?.frozen && (
|
{data.node?.frozen && (
|
||||||
<div className="pr-1">
|
<div className="pr-1">
|
||||||
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
|
<SnowflakeIcon />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={data.node?.frozen ? "text-ice" : ""}>
|
<span className={data.node?.frozen ? "text-ice" : ""}>
|
||||||
<OutputComponent
|
<MemoizedOutputComponent
|
||||||
proxy={outputProxy}
|
proxy={outputProxy}
|
||||||
idx={index}
|
idx={index}
|
||||||
types={type?.split("|") ?? []}
|
types={type?.split("|") ?? []}
|
||||||
|
|
@ -224,6 +306,7 @@ export default function NodeOutputField({
|
||||||
isToolMode={isToolMode}
|
isToolMode={isToolMode}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
content={
|
content={
|
||||||
displayOutputPreview
|
displayOutputPreview
|
||||||
|
|
@ -239,36 +322,25 @@ export default function NodeOutputField({
|
||||||
nodeId={flowPoolId}
|
nodeId={flowPoolId}
|
||||||
outputName={internalOutputName}
|
outputName={internalOutputName}
|
||||||
>
|
>
|
||||||
<Button
|
<InspectButton
|
||||||
disabled={!displayOutputPreview || unknownOutput}
|
disabled={!displayOutputPreview || unknownOutput}
|
||||||
data-testid={`output-inspection-${title.toLowerCase()}`}
|
displayOutputPreview={displayOutputPreview}
|
||||||
unstyled
|
unknownOutput={unknownOutput ?? false}
|
||||||
>
|
errorOutput={errorOutput ?? false}
|
||||||
{
|
isToolMode={isToolMode}
|
||||||
<IconComponent
|
title={title}
|
||||||
className={cn(
|
onClick={() => {
|
||||||
"icon-size",
|
//just to trigger the memoization
|
||||||
isToolMode
|
}}
|
||||||
? displayOutputPreview && !unknownOutput
|
|
||||||
? "text-background hover:text-secondary-hover"
|
|
||||||
: "cursor-not-allowed text-placeholder-foreground opacity-80"
|
|
||||||
: displayOutputPreview && !unknownOutput
|
|
||||||
? "text-foreground hover:text-primary-hover"
|
|
||||||
: "cursor-not-allowed text-placeholder-foreground opacity-60",
|
|
||||||
errorOutput ? "text-destructive" : "",
|
|
||||||
)}
|
|
||||||
name={"ScanEye"}
|
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</OutputModal>
|
</OutputModal>
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{Handle}
|
{Handle}
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(NodeOutputField);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { getNodeInputColors } from "@/CustomNodes/helpers/get-node-input-colors";
|
import { getNodeInputColors } from "@/CustomNodes/helpers/get-node-input-colors";
|
||||||
import { getNodeInputColorsName } from "@/CustomNodes/helpers/get-node-input-colors-name";
|
import { getNodeInputColorsName } from "@/CustomNodes/helpers/get-node-input-colors-name";
|
||||||
|
import { sortToolModeFields } from "@/CustomNodes/helpers/sort-tool-mode-field";
|
||||||
import getFieldTitle from "@/CustomNodes/utils/get-field-title";
|
import getFieldTitle from "@/CustomNodes/utils/get-field-title";
|
||||||
import { scapedJSONStringfy } from "@/utils/reactflowUtils";
|
import { scapedJSONStringfy } from "@/utils/reactflowUtils";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { sortToolModeFields } from "../..";
|
|
||||||
import NodeInputField from "../NodeInputField";
|
import NodeInputField from "../NodeInputField";
|
||||||
|
|
||||||
const RenderInputParameters = ({
|
const RenderInputParameters = ({
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useDarkStore } from "@/stores/darkStore";
|
import { useDarkStore } from "@/stores/darkStore";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Handle, Position } from "reactflow";
|
import { Handle, Position } from "reactflow";
|
||||||
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
||||||
import {
|
import {
|
||||||
|
|
@ -10,7 +10,147 @@ import {
|
||||||
import { cn, groupByFamily } from "../../../../utils/utils";
|
import { cn, groupByFamily } from "../../../../utils/utils";
|
||||||
import HandleTooltipComponent from "../HandleTooltipComponent";
|
import HandleTooltipComponent from "../HandleTooltipComponent";
|
||||||
|
|
||||||
export default function HandleRenderComponent({
|
const BASE_HANDLE_STYLES = {
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
top: "50%",
|
||||||
|
position: "absolute" as const,
|
||||||
|
zIndex: 30,
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const HandleContent = memo(function HandleContent({
|
||||||
|
isNullHandle,
|
||||||
|
handleColor,
|
||||||
|
accentForegroundColorName,
|
||||||
|
isHovered,
|
||||||
|
openHandle,
|
||||||
|
testIdComplement,
|
||||||
|
title,
|
||||||
|
showNode,
|
||||||
|
left,
|
||||||
|
nodeId,
|
||||||
|
colorName,
|
||||||
|
}: {
|
||||||
|
isNullHandle: boolean;
|
||||||
|
handleColor: string;
|
||||||
|
accentForegroundColorName: string;
|
||||||
|
isHovered: boolean;
|
||||||
|
openHandle: boolean;
|
||||||
|
testIdComplement?: string;
|
||||||
|
title: string;
|
||||||
|
showNode: boolean;
|
||||||
|
left: boolean;
|
||||||
|
nodeId: string;
|
||||||
|
colorName?: string[];
|
||||||
|
}) {
|
||||||
|
// Restore animation effect
|
||||||
|
useEffect(() => {
|
||||||
|
if ((isHovered || openHandle) && !isNullHandle) {
|
||||||
|
const styleSheet = document.createElement("style");
|
||||||
|
styleSheet.id = `pulse-${nodeId}`;
|
||||||
|
styleSheet.textContent = `
|
||||||
|
@keyframes pulseNeon {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||||
|
0 0 2px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 4px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 6px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 8px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 10px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 15px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 20px hsl(var(--datatype-${colorName?.[0]}));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||||
|
0 0 4px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 8px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 12px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 16px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 20px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 25px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 30px hsl(var(--datatype-${colorName?.[0]}));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||||
|
0 0 2px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 4px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 6px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 8px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 10px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 15px hsl(var(--datatype-${colorName?.[0]})),
|
||||||
|
0 0 20px hsl(var(--datatype-${colorName?.[0]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const existingStyle = document.getElementById(`pulse-${nodeId}`);
|
||||||
|
if (existingStyle) {
|
||||||
|
existingStyle.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isHovered, openHandle, isNullHandle, nodeId, colorName]);
|
||||||
|
|
||||||
|
const getNeonShadow = useCallback(
|
||||||
|
(color: string, isActive: boolean) => {
|
||||||
|
if (isNullHandle) return "none";
|
||||||
|
if (!isActive) return `0 0 0 3px hsl(var(--${color}))`;
|
||||||
|
return [
|
||||||
|
"0 0 0 1px hsl(var(--border))",
|
||||||
|
`0 0 2px ${color}`,
|
||||||
|
`0 0 4px ${color}`,
|
||||||
|
`0 0 6px ${color}`,
|
||||||
|
`0 0 8px ${color}`,
|
||||||
|
`0 0 10px ${color}`,
|
||||||
|
`0 0 15px ${color}`,
|
||||||
|
`0 0 20px ${color}`,
|
||||||
|
].join(", ");
|
||||||
|
},
|
||||||
|
[isNullHandle],
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
background: isNullHandle ? "hsl(var(--border))" : handleColor,
|
||||||
|
width: "10px",
|
||||||
|
height: "10px",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
boxShadow: getNeonShadow(
|
||||||
|
accentForegroundColorName,
|
||||||
|
isHovered || openHandle,
|
||||||
|
),
|
||||||
|
animation:
|
||||||
|
(isHovered || openHandle) && !isNullHandle
|
||||||
|
? "pulseNeon 1.1s ease-in-out infinite"
|
||||||
|
: "none",
|
||||||
|
border: isNullHandle ? "2px solid hsl(var(--muted))" : "none",
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
isNullHandle,
|
||||||
|
handleColor,
|
||||||
|
getNeonShadow,
|
||||||
|
accentForegroundColorName,
|
||||||
|
isHovered,
|
||||||
|
openHandle,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid={`div-handle-${testIdComplement}-${title.toLowerCase()}-${
|
||||||
|
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
||||||
|
}`}
|
||||||
|
className="noflow nowheel nopan noselect pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 cursor-crosshair rounded-full"
|
||||||
|
style={contentStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const HandleRenderComponent = memo(function HandleRenderComponent({
|
||||||
left,
|
left,
|
||||||
nodes,
|
nodes,
|
||||||
tooltipTitle = "",
|
tooltipTitle = "",
|
||||||
|
|
@ -35,42 +175,51 @@ export default function HandleRenderComponent({
|
||||||
edges: any;
|
edges: any;
|
||||||
myData: any;
|
myData: any;
|
||||||
colors: string[];
|
colors: string[];
|
||||||
setFilterEdge: any;
|
setFilterEdge: (edges: any) => void;
|
||||||
showNode: any;
|
showNode: boolean;
|
||||||
testIdComplement?: string;
|
testIdComplement?: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
colorName?: string[];
|
colorName?: string[];
|
||||||
}) {
|
}) {
|
||||||
const handleColorName = colorName?.[0] ?? "";
|
const handleColorName = colorName?.[0] ?? "";
|
||||||
|
|
||||||
const accentColorName = `datatype-${handleColorName}`;
|
const accentColorName = `datatype-${handleColorName}`;
|
||||||
const accentForegroundColorName = `${accentColorName}-foreground`;
|
const accentForegroundColorName = `${accentColorName}-foreground`;
|
||||||
|
|
||||||
const setHandleDragging = useFlowStore((state) => state.setHandleDragging);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const setFilterType = useFlowStore((state) => state.setFilterType);
|
const [openTooltip, setOpenTooltip] = useState(false);
|
||||||
const handleDragging = useFlowStore((state) => state.handleDragging);
|
|
||||||
const filterType = useFlowStore((state) => state.filterType);
|
const {
|
||||||
|
setHandleDragging,
|
||||||
|
setFilterType,
|
||||||
|
handleDragging,
|
||||||
|
filterType,
|
||||||
|
onConnect,
|
||||||
|
} = useFlowStore(
|
||||||
|
useCallback(
|
||||||
|
(state) => ({
|
||||||
|
setHandleDragging: state.setHandleDragging,
|
||||||
|
setFilterType: state.setFilterType,
|
||||||
|
handleDragging: state.handleDragging,
|
||||||
|
filterType: state.filterType,
|
||||||
|
onConnect: state.onConnect,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const dark = useDarkStore((state) => state.dark);
|
const dark = useDarkStore((state) => state.dark);
|
||||||
|
|
||||||
const onConnect = useFlowStore((state) => state.onConnect);
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
setHandleDragging(undefined);
|
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
const myId = useMemo(
|
const myId = useMemo(
|
||||||
() => scapedJSONStringfy(proxy ? { ...id, proxy } : id),
|
() => scapedJSONStringfy(proxy ? { ...id, proxy } : id),
|
||||||
[id, proxy],
|
[id, proxy],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getConnection = useMemo(
|
const getConnection = useCallback(
|
||||||
() =>
|
|
||||||
(semiConnection: {
|
(semiConnection: {
|
||||||
source: string | undefined;
|
source?: string;
|
||||||
sourceHandle: string | undefined;
|
sourceHandle?: string;
|
||||||
target: string | undefined;
|
target?: string;
|
||||||
targetHandle: string | undefined;
|
targetHandle?: string;
|
||||||
}) => ({
|
}) => ({
|
||||||
source: semiConnection.source ?? nodeId,
|
source: semiConnection.source ?? nodeId,
|
||||||
sourceHandle: semiConnection.sourceHandle ?? myId,
|
sourceHandle: semiConnection.sourceHandle ?? myId,
|
||||||
|
|
@ -80,75 +229,50 @@ export default function HandleRenderComponent({
|
||||||
[nodeId, myId],
|
[nodeId, myId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sameDraggingNode = useMemo(
|
const {
|
||||||
() => (!left ? handleDragging?.target : handleDragging?.source) === nodeId,
|
sameNode,
|
||||||
[left, handleDragging, nodeId],
|
ownHandle,
|
||||||
);
|
openHandle,
|
||||||
|
filterOpenHandle,
|
||||||
|
filterPresent,
|
||||||
|
currentFilter,
|
||||||
|
isNullHandle,
|
||||||
|
handleColor,
|
||||||
|
} = useMemo(() => {
|
||||||
|
const sameDraggingNode =
|
||||||
|
(!left ? handleDragging?.target : handleDragging?.source) === nodeId;
|
||||||
|
const sameFilterNode =
|
||||||
|
(!left ? filterType?.target : filterType?.source) === nodeId;
|
||||||
|
|
||||||
const ownDraggingHandle = useMemo(
|
const ownDraggingHandle =
|
||||||
() =>
|
|
||||||
handleDragging &&
|
handleDragging &&
|
||||||
(left ? handleDragging?.target : handleDragging?.source) &&
|
(left ? handleDragging?.target : handleDragging?.source) &&
|
||||||
(left ? handleDragging.targetHandle : handleDragging.sourceHandle) ===
|
(left ? handleDragging.targetHandle : handleDragging.sourceHandle) ===
|
||||||
myId,
|
myId;
|
||||||
[handleDragging, left, myId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const sameFilterNode = useMemo(
|
const ownFilterHandle =
|
||||||
() => (!left ? filterType?.target : filterType?.source) === nodeId,
|
|
||||||
[left, filterType, nodeId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const ownFilterHandle = useMemo(
|
|
||||||
() =>
|
|
||||||
filterType &&
|
filterType &&
|
||||||
(left ? filterType?.target : filterType?.source) === nodeId &&
|
(left ? filterType?.target : filterType?.source) === nodeId &&
|
||||||
(left ? filterType.targetHandle : filterType.sourceHandle) === myId,
|
(left ? filterType.targetHandle : filterType.sourceHandle) === myId;
|
||||||
[filterType, left, myId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const sameNode = useMemo(
|
const draggingOpenHandle =
|
||||||
() => sameDraggingNode || sameFilterNode,
|
|
||||||
[sameDraggingNode, sameFilterNode],
|
|
||||||
);
|
|
||||||
const ownHandle = useMemo(
|
|
||||||
() => ownDraggingHandle || ownFilterHandle,
|
|
||||||
[ownDraggingHandle, ownFilterHandle],
|
|
||||||
);
|
|
||||||
|
|
||||||
const draggingOpenHandle = useMemo(
|
|
||||||
() =>
|
|
||||||
handleDragging &&
|
handleDragging &&
|
||||||
(left ? handleDragging.source : handleDragging.target) &&
|
(left ? handleDragging.source : handleDragging.target) &&
|
||||||
!ownDraggingHandle
|
!ownDraggingHandle
|
||||||
? isValidConnection(getConnection(handleDragging), nodes, edges)
|
? isValidConnection(getConnection(handleDragging), nodes, edges)
|
||||||
: false,
|
: false;
|
||||||
[handleDragging, left, ownDraggingHandle, getConnection, nodes, edges],
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterOpenHandle = useMemo(
|
const filterOpenHandle =
|
||||||
() =>
|
|
||||||
filterType &&
|
filterType &&
|
||||||
(left ? filterType.source : filterType.target) &&
|
(left ? filterType.source : filterType.target) &&
|
||||||
!ownFilterHandle
|
!ownFilterHandle
|
||||||
? isValidConnection(getConnection(filterType), nodes, edges)
|
? isValidConnection(getConnection(filterType), nodes, edges)
|
||||||
: false,
|
: false;
|
||||||
[filterType, left, ownFilterHandle, getConnection, nodes, edges],
|
|
||||||
);
|
|
||||||
|
|
||||||
const openHandle = useMemo(
|
const openHandle = filterOpenHandle || draggingOpenHandle;
|
||||||
() => filterOpenHandle || draggingOpenHandle,
|
const filterPresent = handleDragging || filterType;
|
||||||
[filterOpenHandle, draggingOpenHandle],
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterPresent = useMemo(
|
const currentFilter = left
|
||||||
() => handleDragging || filterType,
|
|
||||||
[handleDragging, filterType],
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentFilter = useMemo(
|
|
||||||
() =>
|
|
||||||
left
|
|
||||||
? {
|
? {
|
||||||
targetHandle: myId,
|
targetHandle: myId,
|
||||||
target: nodeId,
|
target: nodeId,
|
||||||
|
|
@ -164,15 +288,12 @@ export default function HandleRenderComponent({
|
||||||
targetHandle: undefined,
|
targetHandle: undefined,
|
||||||
type: tooltipTitle,
|
type: tooltipTitle,
|
||||||
color: handleColorName,
|
color: handleColorName,
|
||||||
},
|
};
|
||||||
[left, myId, nodeId, tooltipTitle, colors],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isNullHandle = filterPresent && !(openHandle || ownHandle);
|
const isNullHandle =
|
||||||
|
filterPresent && !(openHandle || ownDraggingHandle || ownFilterHandle);
|
||||||
|
|
||||||
const handleColor = useMemo(
|
const handleColor = isNullHandle
|
||||||
() =>
|
|
||||||
isNullHandle
|
|
||||||
? dark
|
? dark
|
||||||
? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)"
|
? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)"
|
||||||
: "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)"
|
: "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)"
|
||||||
|
|
@ -189,82 +310,49 @@ export default function HandleRenderComponent({
|
||||||
"deg",
|
"deg",
|
||||||
)
|
)
|
||||||
.join(" ,") +
|
.join(" ,") +
|
||||||
")",
|
")";
|
||||||
[filterPresent, openHandle, ownHandle, dark, colors],
|
|
||||||
|
return {
|
||||||
|
sameNode: sameDraggingNode || sameFilterNode,
|
||||||
|
ownHandle: ownDraggingHandle || ownFilterHandle,
|
||||||
|
openHandle,
|
||||||
|
filterOpenHandle,
|
||||||
|
filterPresent,
|
||||||
|
currentFilter,
|
||||||
|
isNullHandle,
|
||||||
|
handleColor,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
left,
|
||||||
|
handleDragging,
|
||||||
|
filterType,
|
||||||
|
nodeId,
|
||||||
|
myId,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
getConnection,
|
||||||
|
dark,
|
||||||
|
colors,
|
||||||
|
colorName,
|
||||||
|
tooltipTitle,
|
||||||
|
handleColorName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(event: React.MouseEvent) => {
|
||||||
|
if (event.button === 0) {
|
||||||
|
setHandleDragging(currentFilter);
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setHandleDragging(undefined);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentFilter, setHandleDragging],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const handleClick = useCallback(() => {
|
||||||
const [openTooltip, setOpenTooltip] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ((isHovered || openHandle) && !isNullHandle) {
|
|
||||||
const styleSheet = document.createElement("style");
|
|
||||||
styleSheet.id = `pulse-${nodeId}`;
|
|
||||||
styleSheet.textContent = `
|
|
||||||
@keyframes pulseNeon {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
|
||||||
0 0 2px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 4px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 6px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 8px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 10px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 15px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 20px hsl(var(--datatype-${colorName![0]}));
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
|
||||||
0 0 4px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 8px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 12px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 16px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 20px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 25px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 30px hsl(var(--datatype-${colorName![0]}));
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
|
||||||
0 0 2px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 4px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 6px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 8px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 10px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 15px hsl(var(--datatype-${colorName![0]})),
|
|
||||||
0 0 20px hsl(var(--datatype-${colorName![0]}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(styleSheet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function should always be returned
|
|
||||||
return () => {
|
|
||||||
const existingStyle = document.getElementById(`pulse-${nodeId}`);
|
|
||||||
if (existingStyle) {
|
|
||||||
existingStyle.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isHovered, openHandle, isNullHandle, colors, nodeId]);
|
|
||||||
|
|
||||||
const getNeonShadow = (color: string, isHovered: boolean) => {
|
|
||||||
if (isNullHandle) return "none";
|
|
||||||
if (!isHovered && !openHandle) return `0 0 0 3px hsl(var(--${color}))`;
|
|
||||||
return [
|
|
||||||
"0 0 0 1px hsl(var(--border))",
|
|
||||||
`0 0 2px ${color}`,
|
|
||||||
`0 0 4px ${color}`,
|
|
||||||
`0 0 6px ${color}`,
|
|
||||||
`0 0 8px ${color}`,
|
|
||||||
`0 0 10px ${color}`,
|
|
||||||
`0 0 15px ${color}`,
|
|
||||||
`0 0 20px ${color}`,
|
|
||||||
].join(", ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRef = useRef<HTMLDivElement>(null);
|
|
||||||
const invisibleDivRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!));
|
setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!));
|
||||||
setFilterType(currentFilter);
|
setFilterType(currentFilter);
|
||||||
if (filterOpenHandle && filterType) {
|
if (filterOpenHandle && filterType) {
|
||||||
|
|
@ -272,14 +360,40 @@ export default function HandleRenderComponent({
|
||||||
setFilterType(undefined);
|
setFilterType(undefined);
|
||||||
setFilterEdge([]);
|
setFilterEdge([]);
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
myData,
|
||||||
|
tooltipTitle,
|
||||||
|
left,
|
||||||
|
nodes,
|
||||||
|
setFilterEdge,
|
||||||
|
setFilterType,
|
||||||
|
currentFilter,
|
||||||
|
filterOpenHandle,
|
||||||
|
filterType,
|
||||||
|
onConnect,
|
||||||
|
getConnection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => setIsHovered(true), []);
|
||||||
|
const handleMouseLeave = useCallback(() => setIsHovered(false), []);
|
||||||
|
const handleMouseUp = useCallback(() => setOpenTooltip(false), []);
|
||||||
|
const handleContextMenu = useCallback(
|
||||||
|
(e: React.MouseEvent) => e.preventDefault(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize the validation function
|
||||||
|
const validateConnection = useCallback(
|
||||||
|
(connection: any) => isValidConnection(connection, nodes, edges),
|
||||||
|
[nodes, edges],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
open={openTooltip}
|
open={openTooltip}
|
||||||
setOpen={setOpenTooltip}
|
setOpen={setOpenTooltip}
|
||||||
styleClasses={cn("tooltip-fixed-width custom-scroll nowheel bottom-2 ")}
|
styleClasses={cn("tooltip-fixed-width custom-scroll nowheel bottom-2")}
|
||||||
delayDuration={1000}
|
delayDuration={1000}
|
||||||
content={
|
content={
|
||||||
<HandleTooltipComponent
|
<HandleTooltipComponent
|
||||||
|
|
@ -296,75 +410,42 @@ export default function HandleRenderComponent({
|
||||||
side={left ? "left" : "right"}
|
side={left ? "left" : "right"}
|
||||||
>
|
>
|
||||||
<Handle
|
<Handle
|
||||||
ref={handleRef}
|
|
||||||
data-testid={`handle-${testIdComplement}-${title.toLowerCase()}-${
|
|
||||||
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
|
||||||
}`}
|
|
||||||
type={left ? "target" : "source"}
|
type={left ? "target" : "source"}
|
||||||
position={left ? Position.Left : Position.Right}
|
position={left ? Position.Left : Position.Right}
|
||||||
key={myId}
|
|
||||||
id={myId}
|
id={myId}
|
||||||
isValidConnection={(connection) =>
|
isValidConnection={validateConnection}
|
||||||
isValidConnection(connection, nodes, edges)
|
|
||||||
}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
`group/handle z-50 transition-all`,
|
`group/handle z-50 transition-all`,
|
||||||
!showNode && "no-show",
|
!showNode && "no-show",
|
||||||
)}
|
)}
|
||||||
|
style={BASE_HANDLE_STYLES}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseUp={() => {
|
onMouseUp={handleMouseUp}
|
||||||
setOpenTooltip(false);
|
onContextMenu={handleContextMenu}
|
||||||
}}
|
onMouseDown={handleMouseDown}
|
||||||
onContextMenu={(event) => {
|
onMouseEnter={handleMouseEnter}
|
||||||
event.preventDefault();
|
onMouseLeave={handleMouseLeave}
|
||||||
}}
|
data-testid={`handle-${testIdComplement}-${title.toLowerCase()}-${
|
||||||
onMouseDown={(event) => {
|
|
||||||
if (event.button === 0) {
|
|
||||||
setHandleDragging(currentFilter);
|
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "32px",
|
|
||||||
height: "32px",
|
|
||||||
top: "50%",
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 30,
|
|
||||||
background: "transparent",
|
|
||||||
border: "none",
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid={`div-handle-${testIdComplement}-${title.toLowerCase()}-${
|
|
||||||
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
||||||
}`}
|
}`}
|
||||||
ref={invisibleDivRef}
|
>
|
||||||
className="noflow nowheel nopan noselect pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 cursor-crosshair rounded-full"
|
<HandleContent
|
||||||
style={{
|
isNullHandle={isNullHandle ?? false}
|
||||||
background: isNullHandle ? "hsl(var(--border))" : handleColor,
|
handleColor={handleColor}
|
||||||
width: "10px",
|
accentForegroundColorName={accentForegroundColorName}
|
||||||
height: "10px",
|
isHovered={isHovered}
|
||||||
transition: "all 0.2s",
|
openHandle={openHandle}
|
||||||
boxShadow: getNeonShadow(
|
testIdComplement={testIdComplement}
|
||||||
accentForegroundColorName,
|
title={title}
|
||||||
isHovered || openHandle,
|
showNode={showNode}
|
||||||
),
|
left={left}
|
||||||
animation:
|
nodeId={nodeId}
|
||||||
(isHovered || openHandle) && !isNullHandle
|
colorName={colorName}
|
||||||
? "pulseNeon 1.1s ease-in-out infinite"
|
|
||||||
: "none",
|
|
||||||
border: isNullHandle ? "2px solid hsl(var(--muted))" : "none",
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
onContextMenu={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Handle>
|
</Handle>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default HandleRenderComponent;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { useUpdateNodeInternals } from "reactflow";
|
import { useUpdateNodeInternals } from "reactflow";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
|
|
@ -15,7 +15,7 @@ import useFlowStore from "../../stores/flowStore";
|
||||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
import { useShortcutsStore } from "../../stores/shortcuts";
|
import { useShortcutsStore } from "../../stores/shortcuts";
|
||||||
import { useTypesStore } from "../../stores/typesStore";
|
import { useTypesStore } from "../../stores/typesStore";
|
||||||
import { OutputFieldType, VertexBuildTypeAPI } from "../../types/api";
|
import { VertexBuildTypeAPI } from "../../types/api";
|
||||||
import { NodeDataType } from "../../types/flow";
|
import { NodeDataType } from "../../types/flow";
|
||||||
import { checkHasToolMode } from "../../utils/reactflowUtils";
|
import { checkHasToolMode } from "../../utils/reactflowUtils";
|
||||||
import { classNames, cn } from "../../utils/utils";
|
import { classNames, cn } from "../../utils/utils";
|
||||||
|
|
@ -23,7 +23,6 @@ import { classNames, cn } from "../../utils/utils";
|
||||||
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
|
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
|
||||||
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
||||||
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||||
import sortFields from "../utils/sort-fields";
|
|
||||||
import NodeDescription from "./components/NodeDescription";
|
import NodeDescription from "./components/NodeDescription";
|
||||||
import NodeName from "./components/NodeName";
|
import NodeName from "./components/NodeName";
|
||||||
import { OutputParameter } from "./components/NodeOutputParameter";
|
import { OutputParameter } from "./components/NodeOutputParameter";
|
||||||
|
|
@ -32,27 +31,36 @@ import RenderInputParameters from "./components/RenderInputParameters";
|
||||||
import { NodeIcon } from "./components/nodeIcon";
|
import { NodeIcon } from "./components/nodeIcon";
|
||||||
import { useBuildStatus } from "./hooks/use-get-build-status";
|
import { useBuildStatus } from "./hooks/use-get-build-status";
|
||||||
|
|
||||||
export const sortToolModeFields = (
|
const MemoizedOutputParameter = memo(OutputParameter);
|
||||||
a: string,
|
const MemoizedRenderInputParameters = memo(RenderInputParameters);
|
||||||
b: string,
|
const MemoizedNodeIcon = memo(NodeIcon);
|
||||||
template: any,
|
const MemoizedNodeName = memo(NodeName);
|
||||||
fieldOrder: string[],
|
const MemoizedNodeStatus = memo(NodeStatus);
|
||||||
isToolMode: boolean,
|
const MemoizedNodeDescription = memo(NodeDescription);
|
||||||
) => {
|
|
||||||
if (!isToolMode) return sortFields(a, b, fieldOrder);
|
|
||||||
|
|
||||||
const aToolMode = template[a]?.tool_mode ?? false;
|
const HiddenOutputsButton = memo(
|
||||||
const bToolMode = template[b]?.tool_mode ?? false;
|
({
|
||||||
|
showHiddenOutputs,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
showHiddenOutputs: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) => (
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
className="group flex h-6 w-6 items-center justify-center rounded-full border bg-background hover:border-foreground hover:text-foreground"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name={showHiddenOutputs ? "EyeOff" : "Eye"}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
className="h-4 w-4 text-placeholder-foreground group-hover:text-foreground"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// If one is tool_mode and the other isn't, tool_mode goes last
|
function GenericNode({
|
||||||
if (aToolMode && !bToolMode) return 1;
|
|
||||||
if (!aToolMode && bToolMode) return -1;
|
|
||||||
|
|
||||||
// If both are tool_mode or both aren't, use regular field order
|
|
||||||
return sortFields(a, b, fieldOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function GenericNode({
|
|
||||||
data,
|
data,
|
||||||
selected,
|
selected,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -61,6 +69,14 @@ export default function GenericNode({
|
||||||
xPos?: number;
|
xPos?: number;
|
||||||
yPos?: number;
|
yPos?: number;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const [isOutdated, setIsOutdated] = useState(false);
|
||||||
|
const [isUserEdited, setIsUserEdited] = useState(false);
|
||||||
|
const [borderColor, setBorderColor] = useState<string>("");
|
||||||
|
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||||||
|
const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
|
||||||
|
const [validationStatus, setValidationStatus] =
|
||||||
|
useState<VertexBuildTypeAPI | null>(null);
|
||||||
|
|
||||||
const types = useTypesStore((state) => state.types);
|
const types = useTypesStore((state) => state.types);
|
||||||
const templates = useTypesStore((state) => state.templates);
|
const templates = useTypesStore((state) => state.templates);
|
||||||
const deleteNode = useFlowStore((state) => state.deleteNode);
|
const deleteNode = useFlowStore((state) => state.deleteNode);
|
||||||
|
|
@ -68,11 +84,19 @@ export default function GenericNode({
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
const [isOutdated, setIsOutdated] = useState(false);
|
const edges = useFlowStore((state) => state.edges);
|
||||||
const [isUserEdited, setIsUserEdited] = useState(false);
|
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||||
const [borderColor, setBorderColor] = useState<string>("");
|
const buildStatus = useBuildStatus(data, data.id);
|
||||||
|
|
||||||
const showNode = data.showNode ?? true;
|
const showNode = data.showNode ?? true;
|
||||||
|
|
||||||
|
const getValidationStatus = (data) => {
|
||||||
|
setValidationStatus(data);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutate: validateComponentCode } = usePostValidateComponentCode();
|
||||||
|
|
||||||
const updateNodeCode = useUpdateNodeCode(
|
const updateNodeCode = useUpdateNodeCode(
|
||||||
data?.id,
|
data?.id,
|
||||||
data.node!,
|
data.node!,
|
||||||
|
|
@ -82,6 +106,8 @@ export default function GenericNode({
|
||||||
updateNodeInternals,
|
updateNodeInternals,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
|
||||||
|
|
||||||
if (!data.node!.template) {
|
if (!data.node!.template) {
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title: `Error in component ${data.node!.display_name}`,
|
title: `Error in component ${data.node!.display_name}`,
|
||||||
|
|
@ -94,23 +120,11 @@ export default function GenericNode({
|
||||||
deleteNode(data.id);
|
deleteNode(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
|
const handleUpdateCode = useCallback(() => {
|
||||||
|
|
||||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
|
||||||
|
|
||||||
const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
|
|
||||||
|
|
||||||
const { mutate: validateComponentCode } = usePostValidateComponentCode();
|
|
||||||
|
|
||||||
const edges = useFlowStore((state) => state.edges);
|
|
||||||
|
|
||||||
const handleUpdateCode = () => {
|
|
||||||
setLoadingUpdate(true);
|
setLoadingUpdate(true);
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
// to update we must get the code from the templates in useTypesStore
|
|
||||||
const thisNodeTemplate = templates[data.type]?.template;
|
const thisNodeTemplate = templates[data.type]?.template;
|
||||||
// if the template does not have a code key
|
|
||||||
// return
|
|
||||||
if (!thisNodeTemplate?.code) return;
|
if (!thisNodeTemplate?.code) return;
|
||||||
|
|
||||||
const currentCode = thisNodeTemplate.code.value;
|
const currentCode = thisNodeTemplate.code.value;
|
||||||
|
|
@ -125,50 +139,67 @@ export default function GenericNode({
|
||||||
edges,
|
edges,
|
||||||
data.id,
|
data.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
updateNodeCode(newNode, currentCode, "code", type);
|
updateNodeCode(newNode, currentCode, "code", type);
|
||||||
setLoadingUpdate(false);
|
setLoadingUpdate(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title: "Error updating Compoenent code",
|
title: "Error updating Component code",
|
||||||
list: [
|
list: [
|
||||||
"There was an error updating the Component.",
|
"There was an error updating the Component.",
|
||||||
"If the error persists, please report it on our Discord or GitHub.",
|
"If the error persists, please report it on our Discord or GitHub.",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
console.log(error);
|
console.error(error);
|
||||||
setLoadingUpdate(false);
|
setLoadingUpdate(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
data,
|
||||||
|
templates,
|
||||||
|
edges,
|
||||||
|
updateNodeCode,
|
||||||
|
validateComponentCode,
|
||||||
|
setErrorData,
|
||||||
|
takeSnapshot,
|
||||||
|
]);
|
||||||
|
|
||||||
function handleUpdateCodeWShortcut() {
|
const handleUpdateCodeWShortcut = useCallback(() => {
|
||||||
if (isOutdated && selected) {
|
if (isOutdated && selected) {
|
||||||
handleUpdateCode();
|
handleUpdateCode();
|
||||||
}
|
}
|
||||||
}
|
}, [isOutdated, selected, handleUpdateCode]);
|
||||||
|
|
||||||
const shownOutputs =
|
|
||||||
data.node!.outputs?.filter((output) => !output.hidden) ?? [];
|
|
||||||
|
|
||||||
const hiddenOutputs =
|
|
||||||
data.node!.outputs?.filter((output) => output.hidden) ?? [];
|
|
||||||
|
|
||||||
const update = useShortcutsStore((state) => state.update);
|
const update = useShortcutsStore((state) => state.update);
|
||||||
useHotkeys(update, handleUpdateCodeWShortcut, { preventDefault: true });
|
useHotkeys(update, handleUpdateCodeWShortcut, { preventDefault: true });
|
||||||
|
|
||||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
// Memoized values
|
||||||
|
const isToolMode = useMemo(
|
||||||
|
() =>
|
||||||
|
data.node?.outputs?.some(
|
||||||
|
(output) => output.name === "component_as_tool",
|
||||||
|
) ?? false,
|
||||||
|
[data.node?.outputs],
|
||||||
|
);
|
||||||
|
|
||||||
const [openShowMoreOptions, setOpenShowMoreOptions] = useState(false);
|
const hasToolMode = useMemo(
|
||||||
|
() => checkHasToolMode(data.node?.template ?? {}),
|
||||||
|
[data.node?.template],
|
||||||
|
);
|
||||||
|
|
||||||
const renderOutputs = (outputs) => {
|
const hasOutputs = useMemo(
|
||||||
return outputs.map((output, idx) => (
|
() => data.node?.outputs && data.node.outputs.length > 0,
|
||||||
<OutputParameter
|
[data.node?.outputs],
|
||||||
key={output.name + idx}
|
);
|
||||||
|
|
||||||
|
const renderOutputs = useCallback(
|
||||||
|
(outputs, key?: string) => {
|
||||||
|
return outputs?.map((output, idx) => (
|
||||||
|
<MemoizedOutputParameter
|
||||||
|
key={`${key}-${output.name}-${idx}`}
|
||||||
output={output}
|
output={output}
|
||||||
idx={
|
idx={
|
||||||
data.node!.outputs?.findIndex((out) => out.name === output.name) ??
|
data.node!.outputs?.findIndex((out) => out.name === output.name) ??
|
||||||
|
|
@ -182,13 +213,19 @@ export default function GenericNode({
|
||||||
isToolMode={isToolMode}
|
isToolMode={isToolMode}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
};
|
},
|
||||||
|
[data, types, selected, showNode, isToolMode],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const { shownOutputs, hiddenOutputs } = useMemo(
|
||||||
if (hiddenOutputs && hiddenOutputs.length == 0) {
|
() => ({
|
||||||
setShowHiddenOutputs(false);
|
shownOutputs:
|
||||||
}
|
data.node?.outputs?.filter((output) => !output.hidden) ?? [],
|
||||||
}, [hiddenOutputs]);
|
hiddenOutputs:
|
||||||
|
data.node?.outputs?.filter((output) => output.hidden) ?? [],
|
||||||
|
}),
|
||||||
|
[data.node?.outputs],
|
||||||
|
);
|
||||||
|
|
||||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||||
return selected ? (
|
return selected ? (
|
||||||
|
|
@ -211,7 +248,6 @@ export default function GenericNode({
|
||||||
onCloseAdvancedModal={() => {}}
|
onCloseAdvancedModal={() => {}}
|
||||||
updateNode={handleUpdateCode}
|
updateNode={handleUpdateCode}
|
||||||
isOutdated={isOutdated && isUserEdited}
|
isOutdated={isOutdated && isUserEdited}
|
||||||
setOpenShowMoreOptions={setOpenShowMoreOptions}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -230,20 +266,95 @@ export default function GenericNode({
|
||||||
shortcuts,
|
shortcuts,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isToolMode =
|
useEffect(() => {
|
||||||
data.node?.outputs?.some((output) => output.name === "component_as_tool") ??
|
if (hiddenOutputs && hiddenOutputs.length === 0) {
|
||||||
false;
|
setShowHiddenOutputs(false);
|
||||||
|
}
|
||||||
|
}, [hiddenOutputs]);
|
||||||
|
|
||||||
const buildStatus = useBuildStatus(data, data.id);
|
const renderNodeIcon = useCallback(() => {
|
||||||
const hasOutputs = data.node?.outputs && data.node?.outputs.length > 0;
|
return (
|
||||||
const [validationStatus, setValidationStatus] =
|
<MemoizedNodeIcon
|
||||||
useState<VertexBuildTypeAPI | null>(null);
|
dataType={data.type}
|
||||||
const getValidationStatus = (data) => {
|
showNode={showNode}
|
||||||
setValidationStatus(data);
|
icon={data.node?.icon}
|
||||||
return null;
|
isGroup={!!data.node?.flow}
|
||||||
};
|
hasToolMode={hasToolMode ?? false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [data.type, showNode, data.node?.icon, data.node?.flow, hasToolMode]);
|
||||||
|
|
||||||
const hasToolMode = checkHasToolMode(data.node?.template ?? {});
|
const renderNodeName = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<MemoizedNodeName
|
||||||
|
display_name={data.node?.display_name}
|
||||||
|
nodeId={data.id}
|
||||||
|
selected={selected}
|
||||||
|
showNode={showNode}
|
||||||
|
validationStatus={validationStatus}
|
||||||
|
isOutdated={isOutdated}
|
||||||
|
beta={data.node?.beta || false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
data.node?.display_name,
|
||||||
|
data.id,
|
||||||
|
selected,
|
||||||
|
showNode,
|
||||||
|
validationStatus,
|
||||||
|
isOutdated,
|
||||||
|
data.node?.beta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const renderNodeStatus = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<MemoizedNodeStatus
|
||||||
|
data={data}
|
||||||
|
frozen={data.node?.frozen}
|
||||||
|
showNode={showNode}
|
||||||
|
display_name={data.node?.display_name!}
|
||||||
|
nodeId={data.id}
|
||||||
|
selected={selected}
|
||||||
|
setBorderColor={setBorderColor}
|
||||||
|
buildStatus={buildStatus}
|
||||||
|
isOutdated={isOutdated}
|
||||||
|
isUserEdited={isUserEdited}
|
||||||
|
getValidationStatus={getValidationStatus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
data,
|
||||||
|
showNode,
|
||||||
|
selected,
|
||||||
|
buildStatus,
|
||||||
|
isOutdated,
|
||||||
|
isUserEdited,
|
||||||
|
getValidationStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const renderDescription = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<MemoizedNodeDescription
|
||||||
|
description={data.node?.description}
|
||||||
|
mdClassName={"dark:prose-invert"}
|
||||||
|
nodeId={data.id}
|
||||||
|
selected={selected}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [data.node?.description, data.id, selected]);
|
||||||
|
|
||||||
|
const renderInputParameters = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<MemoizedRenderInputParameters
|
||||||
|
data={data}
|
||||||
|
types={types}
|
||||||
|
isToolMode={isToolMode}
|
||||||
|
showNode={showNode}
|
||||||
|
shownOutputs={shownOutputs}
|
||||||
|
showHiddenOutputs={showHiddenOutputs}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [data, types, isToolMode, showNode, shownOutputs, showHiddenOutputs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(isOutdated && !isUserEdited ? "relative -mt-10" : "")}>
|
<div className={cn(isOutdated && !isUserEdited ? "relative -mt-10" : "")}>
|
||||||
|
|
@ -298,78 +409,27 @@ export default function GenericNode({
|
||||||
className={"generic-node-title-arrangement"}
|
className={"generic-node-title-arrangement"}
|
||||||
data-testid="generic-node-title-arrangement"
|
data-testid="generic-node-title-arrangement"
|
||||||
>
|
>
|
||||||
<NodeIcon
|
{renderNodeIcon()}
|
||||||
dataType={data.type}
|
<div className="generic-node-tooltip-div">{renderNodeName()}</div>
|
||||||
showNode={showNode}
|
|
||||||
icon={data.node?.icon}
|
|
||||||
isGroup={!!data.node?.flow}
|
|
||||||
hasToolMode={hasToolMode ?? false}
|
|
||||||
/>
|
|
||||||
<div className="generic-node-tooltip-div">
|
|
||||||
<NodeName
|
|
||||||
display_name={data.node?.display_name}
|
|
||||||
nodeId={data.id}
|
|
||||||
selected={selected}
|
|
||||||
showNode={showNode}
|
|
||||||
validationStatus={validationStatus}
|
|
||||||
isOutdated={isOutdated}
|
|
||||||
beta={data.node?.beta || false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{!showNode && (
|
{!showNode && (
|
||||||
<>
|
<>
|
||||||
<RenderInputParameters
|
{renderInputParameters()}
|
||||||
data={data}
|
|
||||||
types={types}
|
|
||||||
isToolMode={isToolMode}
|
|
||||||
showNode={showNode}
|
|
||||||
shownOutputs={shownOutputs}
|
|
||||||
showHiddenOutputs={showHiddenOutputs}
|
|
||||||
/>
|
|
||||||
{shownOutputs &&
|
{shownOutputs &&
|
||||||
shownOutputs.length > 0 &&
|
shownOutputs.length > 0 &&
|
||||||
renderOutputs(shownOutputs)}
|
renderOutputs(shownOutputs, "render-outputs")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<NodeStatus
|
{renderNodeStatus()}
|
||||||
data={data}
|
|
||||||
frozen={data.node?.frozen}
|
|
||||||
showNode={showNode}
|
|
||||||
display_name={data.node?.display_name!}
|
|
||||||
nodeId={data.id}
|
|
||||||
selected={selected}
|
|
||||||
setBorderColor={setBorderColor}
|
|
||||||
buildStatus={buildStatus}
|
|
||||||
isOutdated={isOutdated}
|
|
||||||
isUserEdited={isUserEdited}
|
|
||||||
getValidationStatus={getValidationStatus}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{showNode && (
|
{showNode && <div>{renderDescription()}</div>}
|
||||||
<div>
|
|
||||||
<NodeDescription
|
|
||||||
description={data.node?.description}
|
|
||||||
mdClassName={"dark:prose-invert"}
|
|
||||||
nodeId={data.id}
|
|
||||||
selected={selected}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{showNode && (
|
{showNode && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<>
|
<>
|
||||||
<RenderInputParameters
|
{renderInputParameters()}
|
||||||
data={data}
|
|
||||||
types={types}
|
|
||||||
isToolMode={isToolMode}
|
|
||||||
showNode={showNode}
|
|
||||||
shownOutputs={shownOutputs}
|
|
||||||
showHiddenOutputs={showHiddenOutputs}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
|
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
|
||||||
|
|
@ -380,44 +440,13 @@ export default function GenericNode({
|
||||||
</div>
|
</div>
|
||||||
{!showHiddenOutputs &&
|
{!showHiddenOutputs &&
|
||||||
shownOutputs &&
|
shownOutputs &&
|
||||||
shownOutputs.map((output, idx) => (
|
renderOutputs(shownOutputs, "shown")}
|
||||||
<OutputParameter
|
|
||||||
key={`shown-${output.name}-${idx}`}
|
|
||||||
output={output}
|
|
||||||
idx={
|
|
||||||
data.node!.outputs?.findIndex(
|
|
||||||
(out) => out.name === output.name,
|
|
||||||
) ?? idx
|
|
||||||
}
|
|
||||||
lastOutput={idx === shownOutputs.length - 1}
|
|
||||||
data={data}
|
|
||||||
types={types}
|
|
||||||
selected={selected}
|
|
||||||
showNode={showNode}
|
|
||||||
isToolMode={isToolMode}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<div
|
<div
|
||||||
className={cn(showHiddenOutputs ? "" : "h-0 overflow-hidden")}
|
className={cn(showHiddenOutputs ? "" : "h-0 overflow-hidden")}
|
||||||
>
|
>
|
||||||
<div className="block">
|
<div className="block">
|
||||||
{data.node!.outputs?.map((output, idx) => (
|
{renderOutputs(data.node!.outputs, "hidden")}
|
||||||
<OutputParameter
|
|
||||||
key={`hidden-${output.name}-${idx}`}
|
|
||||||
output={output}
|
|
||||||
idx={
|
|
||||||
data.node!.outputs?.findIndex(
|
|
||||||
(out) => out.name === output.name,
|
|
||||||
) ?? idx
|
|
||||||
}
|
|
||||||
lastOutput={idx === (data.node!.outputs?.length ?? 0) - 1}
|
|
||||||
data={data}
|
|
||||||
types={types}
|
|
||||||
selected={selected}
|
|
||||||
showNode={showNode}
|
|
||||||
isToolMode={isToolMode}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hiddenOutputs && hiddenOutputs.length > 0 && (
|
{hiddenOutputs && hiddenOutputs.length > 0 && (
|
||||||
|
|
@ -437,17 +466,10 @@ export default function GenericNode({
|
||||||
: "bottom-[-0.8rem]",
|
: "bottom-[-0.8rem]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Button
|
<HiddenOutputsButton
|
||||||
unstyled
|
showHiddenOutputs={showHiddenOutputs}
|
||||||
className="group flex h-6 w-6 items-center justify-center rounded-full border bg-background hover:border-foreground hover:text-foreground"
|
|
||||||
onClick={() => setShowHiddenOutputs(!showHiddenOutputs)}
|
onClick={() => setShowHiddenOutputs(!showHiddenOutputs)}
|
||||||
>
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name={showHiddenOutputs ? "EyeOff" : "Eye"}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
className="h-4 w-4 text-placeholder-foreground group-hover:text-foreground"
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -458,3 +480,5 @@ export default function GenericNode({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(GenericNode);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import {
|
import { Select, SelectTrigger } from "@/components/ui/select-custom";
|
||||||
Select,
|
|
||||||
SelectContentWithoutPortal,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
} from "@/components/ui/select-custom";
|
|
||||||
import { COLOR_OPTIONS } from "@/constants/constants";
|
import { COLOR_OPTIONS } from "@/constants/constants";
|
||||||
import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem";
|
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
|
|
@ -20,9 +13,12 @@ import { useShortcutsStore } from "@/stores/shortcuts";
|
||||||
import { noteDataType } from "@/types/flow";
|
import { noteDataType } from "@/types/flow";
|
||||||
import { classNames, cn, openInNewTab } from "@/utils/utils";
|
import { classNames, cn, openInNewTab } from "@/utils/utils";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
import { memo, useCallback, useMemo } from "react";
|
||||||
import IconComponent from "../../../components/common/genericIconComponent";
|
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,
|
data,
|
||||||
bgColor,
|
bgColor,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -30,26 +26,36 @@ export default function NoteToolbarComponent({
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
}) {
|
}) {
|
||||||
const setNoticeData = useAlertStore((state) => state.setNoticeData);
|
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);
|
|
||||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
|
||||||
const deleteNode = useFlowStore((state) => state.deleteNode);
|
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
|
||||||
|
|
||||||
function openDocs() {
|
// 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 shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||||
|
|
||||||
|
const openDocs = useCallback(() => {
|
||||||
if (data.node?.documentation) {
|
if (data.node?.documentation) {
|
||||||
return openInNewTab(data.node?.documentation);
|
return openInNewTab(data.node?.documentation);
|
||||||
}
|
}
|
||||||
setNoticeData({
|
setNoticeData({
|
||||||
title: `${data.id} docs is not available at the moment.`,
|
title: `${data.id} docs is not available at the moment.`,
|
||||||
});
|
});
|
||||||
}
|
}, [data.node?.documentation, data.id, setNoticeData]);
|
||||||
|
|
||||||
const handleSelectChange = (event) => {
|
const handleSelectChange = useCallback(
|
||||||
|
(event: string) => {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case "documentation":
|
case "documentation":
|
||||||
openDocs();
|
openDocs();
|
||||||
|
|
@ -63,24 +69,44 @@ export default function NoteToolbarComponent({
|
||||||
setLastCopiedSelection({ nodes: cloneDeep(node), edges: [] });
|
setLastCopiedSelection({ nodes: cloneDeep(node), edges: [] });
|
||||||
break;
|
break;
|
||||||
case "duplicate":
|
case "duplicate":
|
||||||
|
const targetNode = nodes.find((node) => node.id === data.id);
|
||||||
|
if (targetNode) {
|
||||||
paste(
|
paste(
|
||||||
{
|
{
|
||||||
nodes: [nodes.find((node) => node.id === data.id)!],
|
nodes: [targetNode],
|
||||||
edges: [],
|
edges: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 10,
|
y: 10,
|
||||||
paneX: nodes.find((node) => node.id === data.id)?.position.x,
|
paneX: targetNode.position.x,
|
||||||
paneY: nodes.find((node) => node.id === data.id)?.position.y,
|
paneY: targetNode.position.y,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
// the deafult value is allways the first one if none is provided
|
[
|
||||||
|
openDocs,
|
||||||
|
takeSnapshot,
|
||||||
|
deleteNode,
|
||||||
|
data.id,
|
||||||
|
nodes,
|
||||||
|
setLastCopiedSelection,
|
||||||
|
paste,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize the color picker background style
|
||||||
|
const colorPickerStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
backgroundColor: COLOR_OPTIONS[bgColor] ?? "#00000000",
|
||||||
|
}),
|
||||||
|
[bgColor],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className="w-26 noflow nowheel nopan nodelete nodrag h-10">
|
<div className="w-26 noflow nowheel nopan nodelete nodrag h-10">
|
||||||
<span className="isolate inline-flex rounded-md shadow-sm">
|
<span className="isolate inline-flex rounded-md shadow-sm">
|
||||||
<Popover>
|
<Popover>
|
||||||
|
|
@ -92,58 +118,25 @@ export default function NoteToolbarComponent({
|
||||||
className="relative inline-flex items-center rounded-l-md bg-background px-2 py-2 text-foreground shadow-md transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
|
className="relative inline-flex items-center rounded-l-md bg-background px-2 py-2 text-foreground shadow-md transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={colorPickerStyle}
|
||||||
backgroundColor: COLOR_OPTIONS[bgColor] ?? "#00000000",
|
|
||||||
}}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 rounded-full",
|
"h-4 w-4 rounded-full",
|
||||||
COLOR_OPTIONS[bgColor] === null && "border",
|
COLOR_OPTIONS[bgColor] === null && "border",
|
||||||
)}
|
)}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
<PopoverContent side="top" className="w-fit px-2 py-2">
|
<PopoverContent side="top" className="w-fit px-2 py-2">
|
||||||
<div className="flew-row flex gap-3">
|
<ColorPickerButtons
|
||||||
{Object.entries(COLOR_OPTIONS).map(([color, code]) => {
|
bgColor={bgColor}
|
||||||
return (
|
data={data}
|
||||||
<Button
|
setNode={setNode}
|
||||||
data-testid={`color_picker_button_${color}`}
|
/>
|
||||||
unstyled
|
|
||||||
key={color}
|
|
||||||
onClick={() => {
|
|
||||||
setNode(data.id, (old) => ({
|
|
||||||
...old,
|
|
||||||
data: {
|
|
||||||
...old.data,
|
|
||||||
node: {
|
|
||||||
...old.data.node,
|
|
||||||
template: {
|
|
||||||
...old.data.node?.template,
|
|
||||||
backgroundColor: color,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 rounded-full hover:border hover:border-ring",
|
|
||||||
bgColor === color ? "border-2 border-blue-500" : "",
|
|
||||||
code === null && "border",
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: code ?? "#00000000",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<Select onValueChange={handleSelectChange} value="">
|
<Select onValueChange={handleSelectChange} value="">
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<ShadTooltip content="Show More" side="top">
|
<ShadTooltip content="Show More" side="top">
|
||||||
|
|
@ -162,59 +155,13 @@ export default function NoteToolbarComponent({
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContentWithoutPortal>
|
<SelectItems shortcuts={shortcuts} data={data} />
|
||||||
<SelectItem value={"duplicate"}>
|
|
||||||
<ToolbarSelectItem
|
|
||||||
shortcut={
|
|
||||||
shortcuts.find((obj) => obj.name === "Duplicate")?.shortcut!
|
|
||||||
}
|
|
||||||
value={"Duplicate"}
|
|
||||||
icon={"Copy"}
|
|
||||||
dataTestId="copy-button-modal"
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={"copy"}>
|
|
||||||
<ToolbarSelectItem
|
|
||||||
shortcut={
|
|
||||||
shortcuts.find((obj) => obj.name === "Copy")?.shortcut!
|
|
||||||
}
|
|
||||||
value={"Copy"}
|
|
||||||
icon={"Clipboard"}
|
|
||||||
dataTestId="copy-button-modal"
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
value={"documentation"}
|
|
||||||
disabled={data.node?.documentation === ""}
|
|
||||||
>
|
|
||||||
<ToolbarSelectItem
|
|
||||||
shortcut={
|
|
||||||
shortcuts.find((obj) => obj.name === "Docs")?.shortcut!
|
|
||||||
}
|
|
||||||
value={"Docs"}
|
|
||||||
icon={"FileText"}
|
|
||||||
dataTestId="docs-button-modal"
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={"delete"} className="focus:bg-red-400/[.20]">
|
|
||||||
<div className="font-red flex text-status-red">
|
|
||||||
<IconComponent
|
|
||||||
name="Trash2"
|
|
||||||
className="relative top-0.5 mr-2 h-4 w-4"
|
|
||||||
/>{" "}
|
|
||||||
<span className="">Delete</span>{" "}
|
|
||||||
<span className="absolute right-2 top-2 flex items-center justify-center rounded-sm px-1 py-[0.2]">
|
|
||||||
<IconComponent
|
|
||||||
name="Delete"
|
|
||||||
className="h-4 w-4 stroke-2 text-red-400"
|
|
||||||
></IconComponent>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContentWithoutPortal>
|
|
||||||
</Select>
|
</Select>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
NoteToolbarComponent.displayName = "NoteToolbarComponent";
|
||||||
|
|
||||||
|
export default NoteToolbarComponent;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}) => (
|
||||||
|
<div className="flew-row flex gap-3">
|
||||||
|
{Object.entries(COLOR_OPTIONS).map(([color, code]) => (
|
||||||
|
<Button
|
||||||
|
data-testid={`color_picker_button_${color}`}
|
||||||
|
unstyled
|
||||||
|
key={color}
|
||||||
|
onClick={() => {
|
||||||
|
setNode(data.id, (old) => ({
|
||||||
|
...old,
|
||||||
|
data: {
|
||||||
|
...old.data,
|
||||||
|
node: {
|
||||||
|
...old.data.node,
|
||||||
|
template: {
|
||||||
|
...old.data.node?.template,
|
||||||
|
backgroundColor: color,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 rounded-full hover:border hover:border-ring",
|
||||||
|
bgColor === color ? "border-2 border-blue-500" : "",
|
||||||
|
code === null && "border",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: code ?? "#00000000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColorPickerButtons.displayName = "ColorPickerButtons";
|
||||||
|
|
@ -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 }) => (
|
||||||
|
<SelectContentWithoutPortal>
|
||||||
|
<SelectItem value="duplicate">
|
||||||
|
<ToolbarSelectItem
|
||||||
|
shortcut={
|
||||||
|
shortcuts.find((obj) => obj.name === "Duplicate")?.shortcut!
|
||||||
|
}
|
||||||
|
value="Duplicate"
|
||||||
|
icon="Copy"
|
||||||
|
dataTestId="copy-button-modal"
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="copy">
|
||||||
|
<ToolbarSelectItem
|
||||||
|
shortcut={shortcuts.find((obj) => obj.name === "Copy")?.shortcut!}
|
||||||
|
value="Copy"
|
||||||
|
icon="Clipboard"
|
||||||
|
dataTestId="copy-button-modal"
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="documentation"
|
||||||
|
disabled={data.node?.documentation === ""}
|
||||||
|
>
|
||||||
|
<ToolbarSelectItem
|
||||||
|
shortcut={shortcuts.find((obj) => obj.name === "Docs")?.shortcut!}
|
||||||
|
value="Docs"
|
||||||
|
icon="FileText"
|
||||||
|
dataTestId="docs-button-modal"
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="delete" className="focus:bg-red-400/[.20]">
|
||||||
|
<div className="font-red flex text-status-red">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="Trash2"
|
||||||
|
className="relative top-0.5 mr-2 h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span>Delete</span>
|
||||||
|
<span className="absolute right-2 top-2 flex items-center justify-center rounded-sm px-1 py-[0.2]">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="Delete"
|
||||||
|
className="h-4 w-4 stroke-2 text-red-400"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContentWithoutPortal>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
SelectItems.displayName = "SelectItems";
|
||||||
21
src/frontend/src/CustomNodes/helpers/sort-tool-mode-field.ts
Normal file
21
src/frontend/src/CustomNodes/helpers/sort-tool-mode-field.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import sortFields from "../utils/sort-fields";
|
||||||
|
|
||||||
|
export const sortToolModeFields = (
|
||||||
|
a: string,
|
||||||
|
b: string,
|
||||||
|
template: any,
|
||||||
|
fieldOrder: string[],
|
||||||
|
isToolMode: boolean,
|
||||||
|
) => {
|
||||||
|
if (!isToolMode) return sortFields(a, b, fieldOrder);
|
||||||
|
|
||||||
|
const aToolMode = template[a]?.tool_mode ?? false;
|
||||||
|
const bToolMode = template[b]?.tool_mode ?? false;
|
||||||
|
|
||||||
|
// If one is tool_mode and the other isn't, tool_mode goes last
|
||||||
|
if (aToolMode && !bToolMode) return 1;
|
||||||
|
if (!aToolMode && bToolMode) return -1;
|
||||||
|
|
||||||
|
// If both are tool_mode or both aren't, use regular field order
|
||||||
|
return sortFields(a, b, fieldOrder);
|
||||||
|
};
|
||||||
|
|
@ -6,6 +6,7 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
import { APIClassType, InputFieldType } from "@/types/api";
|
import { APIClassType, InputFieldType } from "@/types/api";
|
||||||
import { NodeType } from "@/types/flow";
|
import { NodeType } from "@/types/flow";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
import { useUpdateNodeInternals } from "reactflow";
|
import { useUpdateNodeInternals } from "reactflow";
|
||||||
import { mutateTemplate } from "../helpers/mutate-template";
|
import { mutateTemplate } from "../helpers/mutate-template";
|
||||||
|
|
||||||
|
|
@ -32,22 +33,52 @@ const useHandleOnNewValue = ({
|
||||||
) => void;
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
|
|
||||||
const setNode = setNodeExternal ?? useFlowStore((state) => state.setNode);
|
const setNode = setNodeExternal ?? useFlowStore((state) => state.setNode);
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const postTemplateValue = usePostTemplateValue({
|
|
||||||
parameterId: name,
|
|
||||||
nodeId: nodeId,
|
|
||||||
node: node,
|
|
||||||
tool_mode: node.tool_mode ?? false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleOnNewValue: handleOnNewValueType = async (changes, options?) => {
|
// Memoize the postTemplateValue hook to prevent unnecessary re-renders
|
||||||
|
const postTemplateValue = usePostTemplateValue(
|
||||||
|
useMemo(
|
||||||
|
() => ({
|
||||||
|
parameterId: name,
|
||||||
|
nodeId,
|
||||||
|
node,
|
||||||
|
tool_mode: node.tool_mode ?? false,
|
||||||
|
}),
|
||||||
|
[name, nodeId, node, node.tool_mode],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize the node update function
|
||||||
|
const updateNodeState = useCallback(
|
||||||
|
(newNode: APIClassType) => {
|
||||||
|
setNode(
|
||||||
|
nodeId,
|
||||||
|
(oldNode) => {
|
||||||
|
const newData = cloneDeep(oldNode.data);
|
||||||
|
newData.node = newNode;
|
||||||
|
return {
|
||||||
|
...oldNode,
|
||||||
|
data: newData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
() => {
|
||||||
|
updateNodeInternals(nodeId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[nodeId, setNode, updateNodeInternals],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize the handleOnNewValue function
|
||||||
|
const handleOnNewValue: handleOnNewValueType = useCallback(
|
||||||
|
async (changes, options?) => {
|
||||||
const newNode = cloneDeep(node);
|
const newNode = cloneDeep(node);
|
||||||
const template = newNode.template;
|
const template = newNode.template;
|
||||||
|
|
||||||
|
// Debounced tracking
|
||||||
track("Component Edited", { nodeId });
|
track("Component Edited", { nodeId });
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
|
|
@ -71,26 +102,12 @@ const useHandleOnNewValue = ({
|
||||||
const shouldUpdate = parameter.real_time_refresh;
|
const shouldUpdate = parameter.real_time_refresh;
|
||||||
|
|
||||||
const setNodeClass = (newNodeClass: APIClassType) => {
|
const setNodeClass = (newNodeClass: APIClassType) => {
|
||||||
options?.setNodeClass && options.setNodeClass(newNodeClass);
|
options?.setNodeClass?.(newNodeClass);
|
||||||
setNode(
|
updateNodeState(newNodeClass);
|
||||||
nodeId,
|
|
||||||
(oldNode) => {
|
|
||||||
const newData = cloneDeep(oldNode.data);
|
|
||||||
newData.node = newNodeClass;
|
|
||||||
return {
|
|
||||||
...oldNode,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
() => {
|
|
||||||
updateNodeInternals(nodeId);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldUpdate && changes.value !== undefined) {
|
if (shouldUpdate && changes.value !== undefined) {
|
||||||
mutateTemplate(
|
await mutateTemplate(
|
||||||
changes.value,
|
changes.value,
|
||||||
newNode,
|
newNode,
|
||||||
setNodeClass,
|
setNodeClass,
|
||||||
|
|
@ -99,22 +116,18 @@ const useHandleOnNewValue = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNode(
|
updateNodeState(newNode);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
node,
|
||||||
nodeId,
|
nodeId,
|
||||||
(oldNode) => {
|
name,
|
||||||
const newData = cloneDeep(oldNode.data);
|
takeSnapshot,
|
||||||
newData.node = newNode;
|
postTemplateValue,
|
||||||
return {
|
setErrorData,
|
||||||
...oldNode,
|
updateNodeState,
|
||||||
data: newData,
|
],
|
||||||
};
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
() => {
|
|
||||||
updateNodeInternals(nodeId);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return { handleOnNewValue };
|
return { handleOnNewValue };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,42 @@
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef, memo, useMemo } from "react";
|
||||||
import { ShadToolTipType } from "../../../types/components";
|
import { ShadToolTipType } from "../../../types/components";
|
||||||
import { cn } from "../../../utils/utils";
|
import { cn } from "../../../utils/utils";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
|
||||||
|
|
||||||
const ShadTooltip = forwardRef<HTMLDivElement, ShadToolTipType>(
|
// 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,
|
||||||
|
{
|
||||||
|
className?: string;
|
||||||
|
side?: ShadToolTipType["side"];
|
||||||
|
avoidCollisions?: boolean;
|
||||||
|
align?: ShadToolTipType["align"];
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
>((props, ref) => (
|
||||||
|
<TooltipContent
|
||||||
|
ref={ref}
|
||||||
|
className={props.className}
|
||||||
|
side={props.side}
|
||||||
|
avoidCollisions={props.avoidCollisions}
|
||||||
|
align={props.align}
|
||||||
|
sticky="always"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</TooltipContent>
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
MemoizedTooltipContent.displayName = "MemoizedTooltipContent";
|
||||||
|
|
||||||
|
// Memoize the main tooltip component
|
||||||
|
const ShadTooltip = memo(
|
||||||
|
forwardRef<HTMLDivElement, ShadToolTipType>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
content,
|
content,
|
||||||
|
|
@ -19,36 +52,46 @@ const ShadTooltip = forwardRef<HTMLDivElement, ShadToolTipType>(
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
// Early return if no content
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return <>{children}</>;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<Tooltip
|
<Tooltip {...tooltipProps}>
|
||||||
defaultOpen={!children}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
delayDuration={delayDuration}
|
|
||||||
>
|
|
||||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||||
<TooltipContent
|
<MemoizedTooltipContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={tooltipClassName}
|
||||||
"z-[99] max-w-96 bg-tooltip text-[12px] text-tooltip-foreground",
|
|
||||||
styleClasses,
|
|
||||||
)}
|
|
||||||
side={side}
|
side={side}
|
||||||
avoidCollisions={avoidCollisions}
|
avoidCollisions={avoidCollisions}
|
||||||
align={align}
|
align={align}
|
||||||
sticky="always"
|
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</TooltipContent>
|
</MemoizedTooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add display name for dev tools
|
||||||
ShadTooltip.displayName = "ShadTooltip";
|
ShadTooltip.displayName = "ShadTooltip";
|
||||||
|
|
||||||
export default ShadTooltip;
|
export default ShadTooltip;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
|
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
||||||
|
import { APIClassType, InputFieldType } from "@/types/api";
|
||||||
|
import { memo, useCallback, useMemo } from "react";
|
||||||
|
import { InputProps } from "./types";
|
||||||
|
|
||||||
|
// Import components
|
||||||
import TableNodeComponent from "@/components/core/parameterRenderComponent/components/TableNodeComponent";
|
import TableNodeComponent from "@/components/core/parameterRenderComponent/components/TableNodeComponent";
|
||||||
import CodeAreaComponent from "@/components/core/parameterRenderComponent/components/codeAreaComponent";
|
import CodeAreaComponent from "@/components/core/parameterRenderComponent/components/codeAreaComponent";
|
||||||
import SliderComponent from "@/components/core/parameterRenderComponent/components/sliderComponent";
|
import SliderComponent from "@/components/core/parameterRenderComponent/components/sliderComponent";
|
||||||
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
|
||||||
import { APIClassType, InputFieldType } from "@/types/api";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import DictComponent from "./components/dictComponent";
|
import DictComponent from "./components/dictComponent";
|
||||||
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
||||||
import FloatComponent from "./components/floatComponent";
|
import FloatComponent from "./components/floatComponent";
|
||||||
|
|
@ -18,21 +21,24 @@ import PromptAreaComponent from "./components/promptComponent";
|
||||||
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
||||||
import { StrRenderComponent } from "./components/strRenderComponent";
|
import { StrRenderComponent } from "./components/strRenderComponent";
|
||||||
import ToggleShadComponent from "./components/toggleShadComponent";
|
import ToggleShadComponent from "./components/toggleShadComponent";
|
||||||
import { InputProps } from "./types";
|
|
||||||
|
|
||||||
export function ParameterRenderComponent({
|
const MemoizedTableNode = memo(TableNodeComponent);
|
||||||
handleOnNewValue,
|
const MemoizedCodeArea = memo(CodeAreaComponent);
|
||||||
name,
|
const MemoizedSlider = memo(SliderComponent);
|
||||||
nodeId,
|
const MemoizedDict = memo(DictComponent);
|
||||||
templateData,
|
const MemoizedEmpty = memo(EmptyParameterComponent);
|
||||||
templateValue,
|
const MemoizedFloat = memo(FloatComponent);
|
||||||
editNode,
|
const MemoizedInputFile = memo(InputFileComponent);
|
||||||
handleNodeClass,
|
const MemoizedInputList = memo(InputListComponent);
|
||||||
nodeClass,
|
const MemoizedInt = memo(IntComponent);
|
||||||
disabled,
|
const MemoizedKeypairList = memo(KeypairListComponent);
|
||||||
placeholder,
|
const MemoizedLink = memo(LinkComponent);
|
||||||
isToolMode,
|
const MemoizedMultiselect = memo(MultiselectComponent);
|
||||||
}: {
|
const MemoizedPromptArea = memo(PromptAreaComponent);
|
||||||
|
const MemoizedStrRender = memo(StrRenderComponent);
|
||||||
|
const MemoizedToggleShad = memo(ToggleShadComponent);
|
||||||
|
|
||||||
|
interface ParameterRenderProps {
|
||||||
handleOnNewValue: handleOnNewValueType;
|
handleOnNewValue: handleOnNewValueType;
|
||||||
name: string;
|
name: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
|
|
@ -44,16 +50,34 @@ export function ParameterRenderComponent({
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isToolMode?: boolean;
|
isToolMode?: boolean;
|
||||||
}) {
|
}
|
||||||
const id = (
|
|
||||||
|
export const ParameterRenderComponent = memo(function ParameterRenderComponent({
|
||||||
|
handleOnNewValue,
|
||||||
|
name,
|
||||||
|
nodeId,
|
||||||
|
templateData,
|
||||||
|
templateValue,
|
||||||
|
editNode,
|
||||||
|
handleNodeClass,
|
||||||
|
nodeClass,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
isToolMode,
|
||||||
|
}: ParameterRenderProps) {
|
||||||
|
const id = useMemo(
|
||||||
|
() =>
|
||||||
|
(
|
||||||
templateData.type +
|
templateData.type +
|
||||||
"_" +
|
"_" +
|
||||||
(editNode ? "edit_" : "") +
|
(editNode ? "edit_" : "") +
|
||||||
templateData.name
|
templateData.name
|
||||||
).toLowerCase();
|
).toLowerCase(),
|
||||||
|
[templateData.type, templateData.name, editNode],
|
||||||
|
);
|
||||||
|
|
||||||
const renderComponent = (): React.ReactElement<InputProps> => {
|
const baseInputProps = useMemo(
|
||||||
const baseInputProps: InputProps = {
|
() => ({
|
||||||
id,
|
id,
|
||||||
value: templateValue,
|
value: templateValue,
|
||||||
editNode,
|
editNode,
|
||||||
|
|
@ -64,12 +88,27 @@ export function ParameterRenderComponent({
|
||||||
readonly: templateData.readonly,
|
readonly: templateData.readonly,
|
||||||
placeholder,
|
placeholder,
|
||||||
isToolMode,
|
isToolMode,
|
||||||
};
|
}),
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
templateValue,
|
||||||
|
editNode,
|
||||||
|
handleOnNewValue,
|
||||||
|
disabled,
|
||||||
|
nodeClass,
|
||||||
|
handleNodeClass,
|
||||||
|
templateData.readonly,
|
||||||
|
placeholder,
|
||||||
|
isToolMode,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderComponent = useCallback((): React.ReactElement<InputProps> => {
|
||||||
if (TEXT_FIELD_TYPES.includes(templateData.type ?? "")) {
|
if (TEXT_FIELD_TYPES.includes(templateData.type ?? "")) {
|
||||||
if (templateData.list) {
|
if (templateData.list) {
|
||||||
if (!templateData.options) {
|
if (!templateData.options) {
|
||||||
return (
|
return (
|
||||||
<InputListComponent
|
<MemoizedInputList
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
componentName={name}
|
componentName={name}
|
||||||
id={`inputlist_${id}`}
|
id={`inputlist_${id}`}
|
||||||
|
|
@ -78,7 +117,7 @@ export function ParameterRenderComponent({
|
||||||
}
|
}
|
||||||
if (!!templateData.options) {
|
if (!!templateData.options) {
|
||||||
return (
|
return (
|
||||||
<MultiselectComponent
|
<MemoizedMultiselect
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
combobox={templateData.combobox}
|
combobox={templateData.combobox}
|
||||||
options={
|
options={
|
||||||
|
|
@ -92,7 +131,7 @@ export function ParameterRenderComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<StrRenderComponent
|
<MemoizedStrRender
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
templateData={templateData}
|
templateData={templateData}
|
||||||
name={name}
|
name={name}
|
||||||
|
|
@ -101,10 +140,11 @@ export function ParameterRenderComponent({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (templateData.type) {
|
switch (templateData.type) {
|
||||||
case "NestedDict":
|
case "NestedDict":
|
||||||
return (
|
return (
|
||||||
<DictComponent
|
<MemoizedDict
|
||||||
name={name ?? ""}
|
name={name ?? ""}
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
id={`dict_${id}`}
|
id={`dict_${id}`}
|
||||||
|
|
@ -112,7 +152,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "dict":
|
case "dict":
|
||||||
return (
|
return (
|
||||||
<KeypairListComponent
|
<MemoizedKeypairList
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
isList={templateData.list ?? false}
|
isList={templateData.list ?? false}
|
||||||
id={`keypair_${id}`}
|
id={`keypair_${id}`}
|
||||||
|
|
@ -120,7 +160,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "bool":
|
case "bool":
|
||||||
return (
|
return (
|
||||||
<ToggleShadComponent
|
<MemoizedToggleShad
|
||||||
size="medium"
|
size="medium"
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
id={`toggle_${id}`}
|
id={`toggle_${id}`}
|
||||||
|
|
@ -128,7 +168,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "link":
|
case "link":
|
||||||
return (
|
return (
|
||||||
<LinkComponent
|
<MemoizedLink
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
icon={templateData.icon}
|
icon={templateData.icon}
|
||||||
text={templateData.text}
|
text={templateData.text}
|
||||||
|
|
@ -137,7 +177,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "float":
|
case "float":
|
||||||
return (
|
return (
|
||||||
<FloatComponent
|
<MemoizedFloat
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
id={`float_${id}`}
|
id={`float_${id}`}
|
||||||
rangeSpec={templateData.range_spec}
|
rangeSpec={templateData.range_spec}
|
||||||
|
|
@ -145,7 +185,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "int":
|
case "int":
|
||||||
return (
|
return (
|
||||||
<IntComponent
|
<MemoizedInt
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
rangeSpec={templateData.range_spec}
|
rangeSpec={templateData.range_spec}
|
||||||
id={`int_${id}`}
|
id={`int_${id}`}
|
||||||
|
|
@ -153,7 +193,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "file":
|
case "file":
|
||||||
return (
|
return (
|
||||||
<InputFileComponent
|
<MemoizedInputFile
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
fileTypes={templateData.fileTypes}
|
fileTypes={templateData.fileTypes}
|
||||||
id={`inputfile_${id}`}
|
id={`inputfile_${id}`}
|
||||||
|
|
@ -161,7 +201,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "prompt":
|
case "prompt":
|
||||||
return (
|
return (
|
||||||
<PromptAreaComponent
|
<MemoizedPromptArea
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
readonly={!!nodeClass.flow}
|
readonly={!!nodeClass.flow}
|
||||||
field_name={name}
|
field_name={name}
|
||||||
|
|
@ -169,10 +209,10 @@ export function ParameterRenderComponent({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "code":
|
case "code":
|
||||||
return <CodeAreaComponent {...baseInputProps} id={`codearea_${id}`} />;
|
return <MemoizedCodeArea {...baseInputProps} id={`codearea_${id}`} />;
|
||||||
case "table":
|
case "table":
|
||||||
return (
|
return (
|
||||||
<TableNodeComponent
|
<MemoizedTableNode
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
description={templateData.info || "Add or edit data"}
|
description={templateData.info || "Add or edit data"}
|
||||||
columns={templateData?.table_schema?.columns}
|
columns={templateData?.table_schema?.columns}
|
||||||
|
|
@ -184,7 +224,7 @@ export function ParameterRenderComponent({
|
||||||
);
|
);
|
||||||
case "slider":
|
case "slider":
|
||||||
return (
|
return (
|
||||||
<SliderComponent
|
<MemoizedSlider
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
value={templateValue}
|
value={templateValue}
|
||||||
rangeSpec={templateData.range_spec}
|
rangeSpec={templateData.range_spec}
|
||||||
|
|
@ -199,12 +239,11 @@ export function ParameterRenderComponent({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return <EmptyParameterComponent {...baseInputProps} />;
|
return <MemoizedEmpty {...baseInputProps} />;
|
||||||
}
|
}
|
||||||
};
|
}, [templateData, baseInputProps, name, id, nodeClass.flow]);
|
||||||
|
|
||||||
return useMemo(
|
return (
|
||||||
() => (
|
|
||||||
<RefreshParameterComponent
|
<RefreshParameterComponent
|
||||||
templateData={templateData}
|
templateData={templateData}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -214,9 +253,7 @@ export function ParameterRenderComponent({
|
||||||
handleNodeClass={handleNodeClass}
|
handleNodeClass={handleNodeClass}
|
||||||
name={name}
|
name={name}
|
||||||
>
|
>
|
||||||
{renderComponent()}
|
{useMemo(() => renderComponent(), [renderComponent])}
|
||||||
</RefreshParameterComponent>
|
</RefreshParameterComponent>
|
||||||
),
|
|
||||||
[templateData, disabled, nodeId, editNode, nodeClass, name, templateValue],
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@ import {
|
||||||
Variants,
|
Variants,
|
||||||
} from "framer-motion";
|
} from "framer-motion";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { createContext, useContext, useEffect, useId, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { cn } from "../../utils/utils";
|
import { cn } from "../../utils/utils";
|
||||||
|
|
||||||
type DisclosureContextType = {
|
type DisclosureContextType = {
|
||||||
|
|
@ -28,38 +36,33 @@ type DisclosureProviderProps = {
|
||||||
variants?: { expanded: Variant; collapsed: Variant };
|
variants?: { expanded: Variant; collapsed: Variant };
|
||||||
};
|
};
|
||||||
|
|
||||||
function DisclosureProvider({
|
const DisclosureProvider = memo(function DisclosureProvider({
|
||||||
children,
|
children,
|
||||||
open: openProp,
|
open: openProp,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
variants,
|
variants,
|
||||||
}: DisclosureProviderProps) {
|
}: DisclosureProviderProps) {
|
||||||
const [internalOpenValue, setInternalOpenValue] = useState<boolean>(openProp);
|
const toggle = useCallback(() => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setInternalOpenValue(openProp);
|
|
||||||
}, [openProp]);
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
const newOpen = !internalOpenValue;
|
|
||||||
setInternalOpenValue(newOpen);
|
|
||||||
if (onOpenChange) {
|
if (onOpenChange) {
|
||||||
onOpenChange(newOpen);
|
onOpenChange(!openProp);
|
||||||
}
|
}
|
||||||
};
|
}, [onOpenChange, openProp]);
|
||||||
|
|
||||||
return (
|
const contextValue = useMemo(
|
||||||
<DisclosureContext.Provider
|
() => ({
|
||||||
value={{
|
open: openProp,
|
||||||
open: internalOpenValue,
|
|
||||||
toggle,
|
toggle,
|
||||||
variants,
|
variants,
|
||||||
}}
|
}),
|
||||||
>
|
[openProp, toggle, variants],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DisclosureContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
</DisclosureContext.Provider>
|
</DisclosureContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
function useDisclosure() {
|
function useDisclosure() {
|
||||||
const context = useContext(DisclosureContext);
|
const context = useContext(DisclosureContext);
|
||||||
|
|
@ -78,7 +81,7 @@ type DisclosureProps = {
|
||||||
transition?: Transition;
|
transition?: Transition;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Disclosure({
|
export const Disclosure = memo(function Disclosure({
|
||||||
open: openProp = false,
|
open: openProp = false,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
children,
|
children,
|
||||||
|
|
@ -86,6 +89,8 @@ export function Disclosure({
|
||||||
transition,
|
transition,
|
||||||
variants,
|
variants,
|
||||||
}: DisclosureProps) {
|
}: DisclosureProps) {
|
||||||
|
const childrenArray = React.Children.toArray(children);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MotionConfig transition={transition}>
|
<MotionConfig transition={transition}>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
|
@ -94,15 +99,15 @@ export function Disclosure({
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
variants={variants}
|
variants={variants}
|
||||||
>
|
>
|
||||||
{React.Children.toArray(children)[0]}
|
{childrenArray[0]}
|
||||||
{React.Children.toArray(children)[1]}
|
{childrenArray[1]}
|
||||||
</DisclosureProvider>
|
</DisclosureProvider>
|
||||||
</div>
|
</div>
|
||||||
</MotionConfig>
|
</MotionConfig>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export function DisclosureTrigger({
|
const DisclosureTrigger = memo(function DisclosureTrigger({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -111,34 +116,54 @@ export function DisclosureTrigger({
|
||||||
}) {
|
}) {
|
||||||
const { toggle, open } = useDisclosure();
|
const { toggle, open } = useDisclosure();
|
||||||
|
|
||||||
return (
|
const handleKeyDown = useCallback(
|
||||||
<>
|
(e: { key: string; preventDefault: () => void }) => {
|
||||||
{React.Children.map(children, (child) => {
|
|
||||||
return React.isValidElement(child)
|
|
||||||
? React.cloneElement(child, {
|
|
||||||
onClick: toggle,
|
|
||||||
role: "button",
|
|
||||||
"aria-expanded": open,
|
|
||||||
tabIndex: 0,
|
|
||||||
onKeyDown: (e: { key: string; preventDefault: () => void }) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggle();
|
toggle();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
className: cn(
|
[toggle],
|
||||||
className,
|
);
|
||||||
(child as React.ReactElement).props.className,
|
|
||||||
),
|
const childProps = useMemo(
|
||||||
...(child as React.ReactElement).props,
|
() => ({
|
||||||
})
|
onClick: toggle,
|
||||||
: child;
|
role: "button",
|
||||||
|
"aria-expanded": open,
|
||||||
|
tabIndex: 0,
|
||||||
|
onKeyDown: handleKeyDown,
|
||||||
|
}),
|
||||||
|
[toggle, open, handleKeyDown],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{React.Children.map(children, (child) => {
|
||||||
|
if (!React.isValidElement(child)) return child;
|
||||||
|
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
...childProps,
|
||||||
|
className: cn(className, child.props.className),
|
||||||
|
...child.props,
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export function DisclosureContent({
|
const BASE_VARIANTS: Variants = {
|
||||||
|
expanded: {
|
||||||
|
height: "auto",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
height: 0,
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DisclosureContent = memo(function DisclosureContent({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -148,21 +173,13 @@ export function DisclosureContent({
|
||||||
const { open, variants } = useDisclosure();
|
const { open, variants } = useDisclosure();
|
||||||
const uniqueId = useId();
|
const uniqueId = useId();
|
||||||
|
|
||||||
const BASE_VARIANTS: Variants = {
|
const combinedVariants = useMemo(
|
||||||
expanded: {
|
() => ({
|
||||||
height: "auto",
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
collapsed: {
|
|
||||||
height: 0,
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const combinedVariants = {
|
|
||||||
expanded: { ...BASE_VARIANTS.expanded, ...variants?.expanded },
|
expanded: { ...BASE_VARIANTS.expanded, ...variants?.expanded },
|
||||||
collapsed: { ...BASE_VARIANTS.collapsed, ...variants?.collapsed },
|
collapsed: { ...BASE_VARIANTS.collapsed, ...variants?.collapsed },
|
||||||
};
|
}),
|
||||||
|
[variants],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("overflow-hidden", className)}>
|
<div className={cn("overflow-hidden", className)}>
|
||||||
|
|
@ -181,7 +198,9 @@ export function DisclosureContent({
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export { DisclosureContent, DisclosureTrigger };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Disclosure,
|
Disclosure,
|
||||||
|
|
|
||||||
|
|
@ -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<any>,
|
||||||
|
data: { type: string; node?: APIClassType },
|
||||||
|
) => void;
|
||||||
|
sensitiveSort: (a: any, b: any) => number;
|
||||||
|
}) {
|
||||||
|
const handleKeyDownInput = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
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 (
|
||||||
|
<Disclosure
|
||||||
|
open={openCategories.includes(item.name)}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
setOpenCategories((prev) =>
|
||||||
|
isOpen
|
||||||
|
? [...prev, item.name]
|
||||||
|
: prev.filter((cat) => cat !== item.name),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DisclosureTrigger className="group/collapsible">
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<div
|
||||||
|
data-testid={`disclosure-${item.display_name.toLocaleLowerCase()}`}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={handleKeyDownInput}
|
||||||
|
className="flex cursor-pointer items-center gap-2"
|
||||||
|
>
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name={item.icon}
|
||||||
|
className="h-4 w-4 group-aria-expanded/collapsible:text-accent-pink-foreground"
|
||||||
|
/>
|
||||||
|
<span className="flex-1 group-aria-expanded/collapsible:font-semibold">
|
||||||
|
{item.display_name}
|
||||||
|
</span>
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="ChevronRight"
|
||||||
|
className="-mr-1 h-4 w-4 text-muted-foreground transition-all group-aria-expanded/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DisclosureTrigger>
|
||||||
|
<DisclosureContent>
|
||||||
|
<SidebarItemsList
|
||||||
|
item={item}
|
||||||
|
dataFilter={dataFilter}
|
||||||
|
nodeColors={nodeColors}
|
||||||
|
chatInputAdded={chatInputAdded}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
sensitiveSort={sensitiveSort}
|
||||||
|
/>
|
||||||
|
</DisclosureContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Disclosure>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CategoryDisclosure.displayName = "CategoryDisclosure";
|
||||||
|
|
@ -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 (
|
||||||
|
<SidebarGroup className="p-3">
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{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 && (
|
||||||
|
<CategoryDisclosure
|
||||||
|
key={item.name}
|
||||||
|
item={item}
|
||||||
|
openCategories={openCategories}
|
||||||
|
setOpenCategories={setOpenCategories}
|
||||||
|
dataFilter={dataFilter}
|
||||||
|
nodeColors={nodeColors}
|
||||||
|
chatInputAdded={chatInputAdded}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
sensitiveSort={sensitiveSort}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CategoryGroup.displayName = "CategoryGroup";
|
||||||
|
|
@ -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<HTMLInputElement>;
|
||||||
|
isInputFocused: boolean;
|
||||||
|
search: string;
|
||||||
|
handleInputFocus: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
handleInputBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full flex-1">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="Search"
|
||||||
|
className="absolute inset-y-0 left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-primary"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
|
type="search"
|
||||||
|
data-testid="sidebar-search-input"
|
||||||
|
className="w-full rounded-lg bg-background pl-8 text-sm"
|
||||||
|
placeholder=""
|
||||||
|
onFocus={handleInputFocus}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={search}
|
||||||
|
/>
|
||||||
|
{!isInputFocused && search === "" && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 left-8 top-1/2 flex w-4/5 -translate-y-1/2 items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||||
|
Search{" "}
|
||||||
|
<span>
|
||||||
|
<ShortcutDisplay sidebar shortcut="/" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SearchInput.displayName = "SearchInput";
|
||||||
|
|
@ -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 (
|
||||||
|
<SidebarHeader className="flex w-full flex-col gap-4 p-4 pb-1">
|
||||||
|
<Disclosure open={showConfig} onOpenChange={setShowConfig}>
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
<SidebarTrigger className="text-muted-foreground">
|
||||||
|
<ForwardedIconComponent name="PanelLeftClose" />
|
||||||
|
</SidebarTrigger>
|
||||||
|
<h3 className="flex-1 text-sm font-semibold">Components</h3>
|
||||||
|
<DisclosureTrigger>
|
||||||
|
<div>
|
||||||
|
<ShadTooltip content="Component settings" styleClasses="z-50">
|
||||||
|
<Button
|
||||||
|
variant={showConfig ? "ghostActive" : "ghost"}
|
||||||
|
size="iconMd"
|
||||||
|
data-testid="sidebar-options-trigger"
|
||||||
|
>
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="SlidersHorizontal"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
</DisclosureTrigger>
|
||||||
|
</div>
|
||||||
|
<DisclosureContent>
|
||||||
|
<FeatureToggles
|
||||||
|
showBeta={showBeta}
|
||||||
|
setShowBeta={setShowBeta}
|
||||||
|
showLegacy={showLegacy}
|
||||||
|
setShowLegacy={setShowLegacy}
|
||||||
|
/>
|
||||||
|
</DisclosureContent>
|
||||||
|
</Disclosure>
|
||||||
|
<SearchInput
|
||||||
|
searchInputRef={searchInputRef}
|
||||||
|
isInputFocused={isInputFocused}
|
||||||
|
search={search}
|
||||||
|
handleInputFocus={handleInputFocus}
|
||||||
|
handleInputBlur={handleInputBlur}
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
{filterType && (
|
||||||
|
<SidebarFilterComponent
|
||||||
|
isInput={!!filterType.source}
|
||||||
|
type={filterType.type}
|
||||||
|
color={filterType.color}
|
||||||
|
resetFilters={() => {
|
||||||
|
setFilterEdge([]);
|
||||||
|
setFilterData(data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SidebarHeader>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SidebarHeaderComponent.displayName = "SidebarHeaderComponent";
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
import Fuse from "fuse.js";
|
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 { useHotkeys } from "react-hotkeys-hook"; // Import useHotkeys
|
||||||
|
|
||||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Disclosure,
|
Disclosure,
|
||||||
DisclosureContent,
|
DisclosureContent,
|
||||||
DisclosureTrigger,
|
DisclosureTrigger,
|
||||||
} from "@/components/ui/disclosure";
|
} from "@/components/ui/disclosure";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
|
|
@ -18,12 +15,9 @@ import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSkeleton,
|
|
||||||
SidebarTrigger,
|
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useAddComponent } from "@/hooks/useAddComponent";
|
import { useAddComponent } from "@/hooks/useAddComponent";
|
||||||
|
|
@ -39,12 +33,11 @@ import useAlertStore from "../../../../stores/alertStore";
|
||||||
import useFlowStore from "../../../../stores/flowStore";
|
import useFlowStore from "../../../../stores/flowStore";
|
||||||
import { useTypesStore } from "../../../../stores/typesStore";
|
import { useTypesStore } from "../../../../stores/typesStore";
|
||||||
import { APIClassType } from "../../../../types/api";
|
import { APIClassType } from "../../../../types/api";
|
||||||
import { SidebarFilterComponent } from "../extraSidebarComponent/sidebarFilterComponent";
|
|
||||||
import sensitiveSort from "../extraSidebarComponent/utils/sensitive-sort";
|
import sensitiveSort from "../extraSidebarComponent/utils/sensitive-sort";
|
||||||
import ShortcutDisplay from "../nodeToolbarComponent/shortcutDisplay";
|
import { CategoryGroup } from "./components/categoryGroup";
|
||||||
import NoResultsMessage from "./components/emptySearchComponent";
|
import NoResultsMessage from "./components/emptySearchComponent";
|
||||||
import FeatureToggles from "./components/featureTogglesComponent";
|
|
||||||
import SidebarMenuButtons from "./components/sidebarFooterButtons";
|
import SidebarMenuButtons from "./components/sidebarFooterButtons";
|
||||||
|
import { SidebarHeaderComponent } from "./components/sidebarHeader";
|
||||||
import SidebarItemsList from "./components/sidebarItemsList";
|
import SidebarItemsList from "./components/sidebarItemsList";
|
||||||
import { applyBetaFilter } from "./helpers/apply-beta-filter";
|
import { applyBetaFilter } from "./helpers/apply-beta-filter";
|
||||||
import { applyEdgeFilter } from "./helpers/apply-edge-filter";
|
import { applyEdgeFilter } from "./helpers/apply-edge-filter";
|
||||||
|
|
@ -58,16 +51,104 @@ const CATEGORIES = SIDEBAR_CATEGORIES;
|
||||||
const BUNDLES = SIDEBAR_BUNDLES;
|
const BUNDLES = SIDEBAR_BUNDLES;
|
||||||
|
|
||||||
export function FlowSidebarComponent() {
|
export function FlowSidebarComponent() {
|
||||||
|
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 hasStore = useStoreStore((state) => state.hasStore);
|
||||||
|
|
||||||
|
// 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<any> | 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<HTMLDivElement>,
|
||||||
|
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 [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const [dataFilter, setFilterData] = useState(data);
|
const [dataFilter, setFilterData] = useState(data);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
@ -231,10 +312,14 @@ export function FlowSidebarComponent() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleSearchInput(e: string) {
|
const handleSearchInput = useCallback(
|
||||||
setSearch(e);
|
(value: string) => {
|
||||||
filterComponents();
|
setSearch(value);
|
||||||
}
|
const filtered = getFilteredData(value, data, fuse);
|
||||||
|
setFilterData(filtered);
|
||||||
|
},
|
||||||
|
[data, fuse],
|
||||||
|
);
|
||||||
|
|
||||||
function onDragStart(
|
function onDragStart(
|
||||||
event: React.DragEvent<any>,
|
event: React.DragEvent<any>,
|
||||||
|
|
@ -252,24 +337,6 @@ export function FlowSidebarComponent() {
|
||||||
event.dataTransfer.setData("genericNode", JSON.stringify(data));
|
event.dataTransfer.setData("genericNode", JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
const customComponent = useMemo(() => {
|
|
||||||
return data?.["custom_component"]?.["CustomComponent"] ?? null;
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const handleKeyDown = (
|
|
||||||
e: React.KeyboardEvent<HTMLDivElement>,
|
|
||||||
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(
|
const hasBundleItems = BUNDLES.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
|
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
|
||||||
|
|
@ -286,9 +353,6 @@ export function FlowSidebarComponent() {
|
||||||
setOpenCategories([]);
|
setOpenCategories([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = useFlowStore((state) => state.nodes);
|
|
||||||
const chatInputAdded = checkChatInput(nodes);
|
|
||||||
|
|
||||||
const handleInputFocus = useCallback(
|
const handleInputFocus = useCallback(
|
||||||
(event: React.FocusEvent<HTMLInputElement>) => {
|
(event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
setIsInputFocused(true);
|
setIsInputFocused(true);
|
||||||
|
|
@ -316,156 +380,40 @@ export function FlowSidebarComponent() {
|
||||||
data-testid="shad-sidebar"
|
data-testid="shad-sidebar"
|
||||||
className="noflow"
|
className="noflow"
|
||||||
>
|
>
|
||||||
<SidebarHeader className="flex w-full flex-col gap-4 p-4 pb-1">
|
<SidebarHeaderComponent
|
||||||
<Disclosure open={showConfig} onOpenChange={setShowConfig}>
|
showConfig={showConfig}
|
||||||
<div className="flex w-full items-center gap-2">
|
setShowConfig={setShowConfig}
|
||||||
<SidebarTrigger className="text-muted-foreground">
|
|
||||||
<ForwardedIconComponent name="PanelLeftClose" />
|
|
||||||
</SidebarTrigger>
|
|
||||||
<h3 className="flex-1 text-sm font-semibold">Components</h3>
|
|
||||||
<DisclosureTrigger>
|
|
||||||
<div>
|
|
||||||
<ShadTooltip content="Component settings" styleClasses="z-50">
|
|
||||||
<Button
|
|
||||||
variant={showConfig ? "ghostActive" : "ghost"}
|
|
||||||
size="iconMd"
|
|
||||||
data-testid="sidebar-options-trigger"
|
|
||||||
>
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="SlidersHorizontal"
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
</div>
|
|
||||||
</DisclosureTrigger>
|
|
||||||
</div>
|
|
||||||
<DisclosureContent>
|
|
||||||
<FeatureToggles
|
|
||||||
showBeta={showBeta}
|
showBeta={showBeta}
|
||||||
setShowBeta={setShowBeta}
|
setShowBeta={setShowBeta}
|
||||||
showLegacy={showLegacy}
|
showLegacy={showLegacy}
|
||||||
setShowLegacy={setShowLegacy}
|
setShowLegacy={setShowLegacy}
|
||||||
|
searchInputRef={searchInputRef}
|
||||||
|
isInputFocused={isInputFocused}
|
||||||
|
search={search}
|
||||||
|
handleInputFocus={handleInputFocus}
|
||||||
|
handleInputBlur={handleInputBlur}
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
filterType={filterType}
|
||||||
|
setFilterEdge={setFilterEdge}
|
||||||
|
setFilterData={setFilterData}
|
||||||
|
data={data}
|
||||||
/>
|
/>
|
||||||
</DisclosureContent>
|
|
||||||
</Disclosure>
|
|
||||||
<div className="relative w-full flex-1">
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="Search"
|
|
||||||
className="absolute inset-y-0 left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-primary"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
ref={searchInputRef}
|
|
||||||
type="search"
|
|
||||||
data-testid="sidebar-search-input"
|
|
||||||
className="w-full rounded-lg bg-background pl-8 text-sm"
|
|
||||||
placeholder=""
|
|
||||||
onFocus={handleInputFocus}
|
|
||||||
onBlur={handleInputBlur}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={search}
|
|
||||||
/>
|
|
||||||
{!isInputFocused && search === "" && (
|
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-8 top-1/2 flex w-4/5 -translate-y-1/2 items-center justify-between gap-2 text-sm text-muted-foreground">
|
|
||||||
Search{" "}
|
|
||||||
<span>
|
|
||||||
<ShortcutDisplay sidebar shortcut="/" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{filterType && (
|
|
||||||
<SidebarFilterComponent
|
|
||||||
isInput={!!filterType.source}
|
|
||||||
type={filterType.type}
|
|
||||||
color={filterType.color}
|
|
||||||
resetFilters={() => {
|
|
||||||
setFilterEdge([]);
|
|
||||||
setFilterData(data);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
{hasResults ? (
|
{hasResults ? (
|
||||||
<>
|
<>
|
||||||
{hasCategoryItems && (
|
{hasCategoryItems && (
|
||||||
<SidebarGroup className="p-3">
|
<CategoryGroup
|
||||||
<SidebarGroupContent>
|
|
||||||
<SidebarMenu>
|
|
||||||
{!data
|
|
||||||
? Array.from({ length: 5 }).map((_, index) => (
|
|
||||||
<SidebarMenuItem key={index}>
|
|
||||||
<SidebarMenuSkeleton />
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))
|
|
||||||
: 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 && (
|
|
||||||
<Disclosure
|
|
||||||
key={item.name}
|
|
||||||
open={openCategories.includes(item.name)}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
setOpenCategories((prev) =>
|
|
||||||
isOpen
|
|
||||||
? [...prev, item.name]
|
|
||||||
: prev.filter((cat) => cat !== item.name),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<DisclosureTrigger className="group/collapsible">
|
|
||||||
<SidebarMenuButton asChild>
|
|
||||||
<div
|
|
||||||
data-testid={`disclosure-${item.display_name.toLocaleLowerCase()}`}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
handleKeyDown(e, item.name)
|
|
||||||
}
|
|
||||||
className="flex cursor-pointer items-center gap-2"
|
|
||||||
>
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name={item.icon}
|
|
||||||
className="h-4 w-4 group-aria-expanded/collapsible:text-accent-pink-foreground"
|
|
||||||
/>
|
|
||||||
<span className="flex-1 group-aria-expanded/collapsible:font-semibold">
|
|
||||||
{item.display_name}
|
|
||||||
</span>
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="ChevronRight"
|
|
||||||
className="-mr-1 h-4 w-4 text-muted-foreground transition-all group-aria-expanded/collapsible:rotate-90"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</DisclosureTrigger>
|
|
||||||
<DisclosureContent>
|
|
||||||
<SidebarItemsList
|
|
||||||
item={item}
|
|
||||||
dataFilter={dataFilter}
|
dataFilter={dataFilter}
|
||||||
|
sortedCategories={sortedCategories}
|
||||||
|
CATEGORIES={CATEGORIES}
|
||||||
|
openCategories={openCategories}
|
||||||
|
setOpenCategories={setOpenCategories}
|
||||||
|
search={search}
|
||||||
nodeColors={nodeColors}
|
nodeColors={nodeColors}
|
||||||
chatInputAdded={chatInputAdded}
|
chatInputAdded={chatInputAdded}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
sensitiveSort={sensitiveSort}
|
sensitiveSort={sensitiveSort}
|
||||||
/>
|
/>
|
||||||
</DisclosureContent>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</Disclosure>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
)}
|
)}
|
||||||
{hasBundleItems && (
|
{hasBundleItems && (
|
||||||
<SidebarGroup className="p-3">
|
<SidebarGroup className="p-3">
|
||||||
|
|
@ -501,7 +449,7 @@ export function FlowSidebarComponent() {
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
handleKeyDown(e, item.name)
|
handleKeyDownInput(e, item.name)
|
||||||
}
|
}
|
||||||
className="flex cursor-pointer items-center gap-2"
|
className="flex cursor-pointer items-center gap-2"
|
||||||
data-testid={`disclosure-bundles-${item.display_name.toLocaleLowerCase()}`}
|
data-testid={`disclosure-bundles-${item.display_name.toLocaleLowerCase()}`}
|
||||||
|
|
@ -553,3 +501,7 @@ export function FlowSidebarComponent() {
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlowSidebarComponent.displayName = "FlowSidebarComponent";
|
||||||
|
|
||||||
|
export default memo(FlowSidebarComponent);
|
||||||
|
|
|
||||||
|
|
@ -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<any>,
|
||||||
|
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<HTMLInputElement>;
|
||||||
|
isInputFocused: boolean;
|
||||||
|
search: string;
|
||||||
|
handleInputFocus: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
handleInputBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => 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<SetStateAction<APIDataType>>;
|
||||||
|
data: APIDataType;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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,
|
||||||
|
dataTestId,
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
icon: string;
|
||||||
|
label?: string;
|
||||||
|
shortcut?: any;
|
||||||
|
className?: string;
|
||||||
|
dataTestId?: string;
|
||||||
|
}) => (
|
||||||
|
<ShadTooltip content={<ShortcutDisplay {...shortcut} />} side="top">
|
||||||
|
<Button
|
||||||
|
className={cn("node-toolbar-buttons", className)}
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onClick}
|
||||||
|
size="node-toolbar"
|
||||||
|
data-testid={dataTestId}
|
||||||
|
>
|
||||||
|
<ForwardedIconComponent name={icon} className="h-4 w-4" />
|
||||||
|
{label && <span className="text-[13px] font-medium">{label}</span>}
|
||||||
|
</Button>
|
||||||
|
</ShadTooltip>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -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 && (
|
||||||
|
<EditNodeModal
|
||||||
|
data={data}
|
||||||
|
open={showModalAdvanced}
|
||||||
|
setOpen={setShowModalAdvanced}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showconfirmShare && (
|
||||||
|
<ShareModal
|
||||||
|
open={showconfirmShare}
|
||||||
|
setOpen={setShowconfirmShare}
|
||||||
|
is_component={true}
|
||||||
|
component={flowComponent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showOverrideModal && (
|
||||||
|
<ConfirmationModal
|
||||||
|
open={showOverrideModal}
|
||||||
|
title="Replace"
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleClose}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
cancelText="Create New"
|
||||||
|
confirmationText="Replace"
|
||||||
|
size="x-small"
|
||||||
|
icon="SaveAll"
|
||||||
|
index={6}
|
||||||
|
>
|
||||||
|
<ConfirmationModal.Content>
|
||||||
|
<span>
|
||||||
|
It seems {data.node?.display_name} already exists. Do you want
|
||||||
|
to replace it with the current or create a new one?
|
||||||
|
</span>
|
||||||
|
</ConfirmationModal.Content>
|
||||||
|
</ConfirmationModal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasCode && (
|
||||||
|
<div className="hidden">
|
||||||
|
{openModal && (
|
||||||
|
<CodeAreaModal
|
||||||
|
setValue={handleOnNewValue}
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
dynamic={true}
|
||||||
|
setNodeClass={(apiClassType, type) => {
|
||||||
|
handleNodeClass(apiClassType, type);
|
||||||
|
setToolMode(false);
|
||||||
|
}}
|
||||||
|
nodeClass={data.node}
|
||||||
|
value={data.node?.template[name]?.value ?? ""}
|
||||||
|
componentId={data.id}
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</CodeAreaModal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ToolbarModals.displayName = "ToolbarModals";
|
||||||
|
|
||||||
|
export default ToolbarModals;
|
||||||
|
|
@ -3,7 +3,6 @@ import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
||||||
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
|
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
|
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
import ToggleShadComponent from "@/components/core/parameterRenderComponent/components/toggleShadComponent";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow";
|
import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow";
|
||||||
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
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 CodeAreaModal from "@/modals/codeAreaModal";
|
||||||
import { APIClassType } from "@/types/api";
|
import { APIClassType } from "@/types/api";
|
||||||
import _, { cloneDeep } from "lodash";
|
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 { useUpdateNodeInternals } from "reactflow";
|
||||||
import IconComponent from "../../../../components/common/genericIconComponent";
|
import IconComponent from "../../../../components/common/genericIconComponent";
|
||||||
import {
|
import {
|
||||||
|
|
@ -40,11 +39,13 @@ import {
|
||||||
updateFlowPosition,
|
updateFlowPosition,
|
||||||
} from "../../../../utils/reactflowUtils";
|
} from "../../../../utils/reactflowUtils";
|
||||||
import { cn, getNodeLength, openInNewTab } from "../../../../utils/utils";
|
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 useShortcuts from "./hooks/use-shortcuts";
|
||||||
import ShortcutDisplay from "./shortcutDisplay";
|
|
||||||
import ToolbarSelectItem from "./toolbarSelectItem";
|
import ToolbarSelectItem from "./toolbarSelectItem";
|
||||||
|
|
||||||
export default function NodeToolbarComponent({
|
const NodeToolbarComponent = memo(
|
||||||
|
({
|
||||||
data,
|
data,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
setShowNode,
|
setShowNode,
|
||||||
|
|
@ -55,7 +56,7 @@ export default function NodeToolbarComponent({
|
||||||
updateNode,
|
updateNode,
|
||||||
isOutdated,
|
isOutdated,
|
||||||
setOpenShowMoreOptions,
|
setOpenShowMoreOptions,
|
||||||
}: nodeToolbarPropsType): JSX.Element {
|
}: nodeToolbarPropsType): JSX.Element => {
|
||||||
const version = useDarkStore((state) => state.version);
|
const version = useDarkStore((state) => state.version);
|
||||||
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
||||||
const [showconfirmShare, setShowconfirmShare] = useState(false);
|
const [showconfirmShare, setShowconfirmShare] = useState(false);
|
||||||
|
|
@ -63,157 +64,20 @@ export default function NodeToolbarComponent({
|
||||||
const [flowComponent, setFlowComponent] = useState<FlowType>(
|
const [flowComponent, setFlowComponent] = useState<FlowType>(
|
||||||
createFlowComponent(cloneDeep(data), version),
|
createFlowComponent(cloneDeep(data), version),
|
||||||
);
|
);
|
||||||
const nodeLength = getNodeLength(data);
|
const updateFreezeStatus = useFlowStore(
|
||||||
const updateFreezeStatus = useFlowStore((state) => state.updateFreezeStatus);
|
(state) => state.updateFreezeStatus,
|
||||||
const hasStore = useStoreStore((state) => state.hasStore);
|
);
|
||||||
const hasApiKey = useStoreStore((state) => state.hasApiKey);
|
const { hasStore, hasApiKey, validApiKey } = useStoreStore((state) => ({
|
||||||
const validApiKey = useStoreStore((state) => state.validApiKey);
|
hasStore: state.hasStore,
|
||||||
|
hasApiKey: state.hasApiKey,
|
||||||
|
validApiKey: state.validApiKey,
|
||||||
|
}));
|
||||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const isGroup = data.node?.flow ? true : false;
|
|
||||||
const frozen = data.node?.frozen ?? false;
|
const frozen = data.node?.frozen ?? false;
|
||||||
const currentFlow = useFlowStore((state) => state.currentFlow);
|
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!,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shareComponent() {
|
|
||||||
if (hasApiKey || hasStore) {
|
|
||||||
setShowconfirmShare((state) => !state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCodeModal() {
|
|
||||||
if (!hasCode)
|
|
||||||
setNoticeData({ title: `You can not access ${data.id} code` });
|
|
||||||
setOpenModal((state) => !state);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 paste = useFlowStore((state) => state.paste);
|
||||||
const nodes = useFlowStore((state) => state.nodes);
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
const edges = useFlowStore((state) => state.edges);
|
const edges = useFlowStore((state) => state.edges);
|
||||||
|
|
@ -232,6 +96,193 @@ export default function NodeToolbarComponent({
|
||||||
});
|
});
|
||||||
const updateToolMode = useFlowStore((state) => state.updateToolMode);
|
const updateToolMode = useFlowStore((state) => state.updateToolMode);
|
||||||
|
|
||||||
|
const isSaved = flows?.some((flow) =>
|
||||||
|
Object.values(flow).includes(data.node?.display_name!),
|
||||||
|
);
|
||||||
|
|
||||||
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
|
||||||
|
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],
|
||||||
|
);
|
||||||
|
|
||||||
|
const addFlow = useAddFlow();
|
||||||
|
|
||||||
|
const { mutate: patchUpdateFlow } = usePatchUpdateFlow();
|
||||||
|
|
||||||
|
const isMinimal = useMemo(
|
||||||
|
() => countHandlesFn(data) <= 1 && numberOfOutputHandles <= 1,
|
||||||
|
[data, numberOfOutputHandles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [toolMode, setToolMode] = useState(() => {
|
||||||
|
// Check if tool mode is explicitly set on the node
|
||||||
|
const hasToolModeProperty = data.node?.tool_mode;
|
||||||
|
if (hasToolModeProperty) {
|
||||||
|
return hasToolModeProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise check if node has component_as_tool output
|
||||||
|
const hasComponentAsTool = data.node?.outputs?.some(
|
||||||
|
(output) => output.name === "component_as_tool",
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasComponentAsTool ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
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!,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const handleungroup = useCallback(() => {
|
||||||
|
if (isGroup) {
|
||||||
|
takeSnapshot();
|
||||||
|
expandGroupNode(
|
||||||
|
data.id,
|
||||||
|
updateFlowPosition(getNodePosition(data.id), data.node?.flow!),
|
||||||
|
data.node!.template,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
data.node?.outputs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isGroup,
|
||||||
|
data.id,
|
||||||
|
data.node?.flow,
|
||||||
|
data.node?.template,
|
||||||
|
data.node?.outputs,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
takeSnapshot,
|
||||||
|
getNodePosition,
|
||||||
|
updateFlowPosition,
|
||||||
|
expandGroupNode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const shareComponent = useCallback(() => {
|
||||||
|
if (hasApiKey || hasStore) {
|
||||||
|
setShowconfirmShare((state) => !state);
|
||||||
|
}
|
||||||
|
}, [hasApiKey, hasStore]);
|
||||||
|
|
||||||
|
const handleCodeModal = useCallback(() => {
|
||||||
|
if (!hasCode) {
|
||||||
|
setNoticeData({ title: `You can not access ${data.id} code` });
|
||||||
|
}
|
||||||
|
setOpenModal((state) => !state);
|
||||||
|
}, [hasCode, data.id]);
|
||||||
|
|
||||||
|
const saveComponent = useCallback(() => {
|
||||||
|
if (isSaved) {
|
||||||
|
setShowOverrideModal((state) => !state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addFlow({
|
||||||
|
flow: flowComponent,
|
||||||
|
override: false,
|
||||||
|
});
|
||||||
|
setSuccessData({ title: `${data.id} saved successfully` });
|
||||||
|
}, [isSaved, data.id, flowComponent, addFlow]);
|
||||||
|
|
||||||
|
const openDocs = useCallback(() => {
|
||||||
|
if (data.node?.documentation) {
|
||||||
|
return openInNewTab(data.node.documentation);
|
||||||
|
}
|
||||||
|
setNoticeData({
|
||||||
|
title: `${data.id} docs is not available at the moment.`,
|
||||||
|
});
|
||||||
|
}, [data.id, data.node?.documentation, openInNewTab]);
|
||||||
|
|
||||||
|
const freezeFunction = useCallback(() => {
|
||||||
|
setNode(data.id, (old) => ({
|
||||||
|
...old,
|
||||||
|
data: {
|
||||||
|
...old.data,
|
||||||
|
node: {
|
||||||
|
...old.data.node,
|
||||||
|
frozen: old.data?.node?.frozen ? false : true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [data.id, setNode]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showModalAdvanced) {
|
if (!showModalAdvanced) {
|
||||||
onCloseAdvancedModal!(false);
|
onCloseAdvancedModal!(false);
|
||||||
|
|
@ -261,7 +312,7 @@ export default function NodeToolbarComponent({
|
||||||
|
|
||||||
const [selectedValue, setSelectedValue] = useState(null);
|
const [selectedValue, setSelectedValue] = useState(null);
|
||||||
|
|
||||||
const handleSelectChange = (event) => {
|
const handleSelectChange = useCallback((event) => {
|
||||||
setSelectedValue(event);
|
setSelectedValue(event);
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
|
@ -282,7 +333,7 @@ export default function NodeToolbarComponent({
|
||||||
break;
|
break;
|
||||||
case "show":
|
case "show":
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
minimize();
|
handleMinimize();
|
||||||
break;
|
break;
|
||||||
case "Share":
|
case "Share":
|
||||||
shareComponent();
|
shareComponent();
|
||||||
|
|
@ -332,18 +383,12 @@ export default function NodeToolbarComponent({
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "toolMode":
|
case "toolMode":
|
||||||
activateToolMode();
|
handleActivateToolMode();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedValue(null);
|
setSelectedValue(null);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const isSaved = flows?.some((flow) =>
|
|
||||||
Object.values(flow).includes(data.node?.display_name!),
|
|
||||||
);
|
|
||||||
|
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
|
||||||
|
|
||||||
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue({
|
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue({
|
||||||
node: data.node!,
|
node: data.node!,
|
||||||
|
|
@ -355,14 +400,14 @@ export default function NodeToolbarComponent({
|
||||||
handleOnNewValueHook({ value });
|
handleOnNewValueHook({ value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(data.id);
|
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
|
||||||
|
data.id,
|
||||||
|
);
|
||||||
|
|
||||||
const handleNodeClass = (newNodeClass: APIClassType, type: string) => {
|
const handleNodeClass = (newNodeClass: APIClassType, type: string) => {
|
||||||
handleNodeClassHook(newNodeClass, type);
|
handleNodeClassHook(newNodeClass, type);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasCode = Object.keys(data.node!.template).includes("code");
|
|
||||||
|
|
||||||
const selectTriggerRef = useRef(null);
|
const selectTriggerRef = useRef(null);
|
||||||
|
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = () => {
|
||||||
|
|
@ -373,21 +418,6 @@ export default function NodeToolbarComponent({
|
||||||
setOpenShowMoreOptions && setOpenShowMoreOptions(open);
|
setOpenShowMoreOptions && setOpenShowMoreOptions(open);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [toolMode, setToolMode] = useState(() => {
|
|
||||||
// Check if tool mode is explicitly set on the node
|
|
||||||
const hasToolModeProperty = data.node?.tool_mode;
|
|
||||||
if (hasToolModeProperty) {
|
|
||||||
return hasToolModeProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise check if node has component_as_tool output
|
|
||||||
const hasComponentAsTool = data.node?.outputs?.some(
|
|
||||||
(output) => output.name === "component_as_tool",
|
|
||||||
);
|
|
||||||
|
|
||||||
return hasComponentAsTool ?? false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const postToolModeValue = usePostTemplateValue({
|
const postToolModeValue = usePostTemplateValue({
|
||||||
node: data.node!,
|
node: data.node!,
|
||||||
nodeId: data.id,
|
nodeId: data.id,
|
||||||
|
|
@ -395,168 +425,87 @@ export default function NodeToolbarComponent({
|
||||||
tool_mode: data.node!.tool_mode ?? false,
|
tool_mode: data.node!.tool_mode ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const renderToolbarButtons = useMemo(
|
||||||
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,
|
|
||||||
});
|
|
||||||
setSuccessData({ title: "New component successfully saved!" });
|
|
||||||
setShowOverrideModal(false);
|
|
||||||
}, [flowComponent, setSuccessData, setShowOverrideModal]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
<div className="noflow nopan nodelete nodrag">
|
|
||||||
<div className="toolbar-wrapper">
|
|
||||||
{hasCode && (
|
{hasCode && (
|
||||||
<ShadTooltip
|
<ToolbarButton
|
||||||
content={
|
icon="Code"
|
||||||
<ShortcutDisplay
|
label="Code"
|
||||||
{...shortcuts.find(
|
onClick={() => setOpenModal(true)}
|
||||||
({ name }) => name.split(" ")[0].toLowerCase() === "code",
|
shortcut={shortcuts.find((s) =>
|
||||||
)!}
|
s.name.toLowerCase().startsWith("code"),
|
||||||
/>
|
|
||||||
}
|
|
||||||
side="top"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="node-toolbar-buttons"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
setOpenModal(!openModal);
|
|
||||||
}}
|
|
||||||
data-testid="code-button-modal"
|
|
||||||
size="node-toolbar"
|
|
||||||
>
|
|
||||||
<IconComponent name="Code" className="h-4 w-4" />
|
|
||||||
|
|
||||||
<span className="text-[13px] font-medium">Code</span>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
)}
|
)}
|
||||||
|
dataTestId="code-button-modal"
|
||||||
{nodeLength > 0 && (
|
/>
|
||||||
<ShadTooltip
|
)}
|
||||||
content={
|
{nodeLength > 0 && (
|
||||||
<ShortcutDisplay
|
<ToolbarButton
|
||||||
{...shortcuts.find(
|
icon="SlidersHorizontal"
|
||||||
({ name }) =>
|
label="Controls"
|
||||||
name.split(" ")[0].toLowerCase() === "advanced",
|
onClick={() => setShowModalAdvanced(true)}
|
||||||
)!}
|
shortcut={shortcuts.find((s) =>
|
||||||
|
s.name.toLowerCase().startsWith("advanced"),
|
||||||
|
)}
|
||||||
|
dataTestId="edit-button-modal"
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
side="top"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="node-toolbar-buttons"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
setShowModalAdvanced(true);
|
|
||||||
}}
|
|
||||||
data-testid="edit-button-modal"
|
|
||||||
size="node-toolbar"
|
|
||||||
>
|
|
||||||
<IconComponent name="SlidersHorizontal" className="h-4 w-4" />
|
|
||||||
<span className="text-[13px] font-medium">Controls</span>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
)}
|
)}
|
||||||
{!hasToolMode && (
|
{!hasToolMode && (
|
||||||
<ShadTooltip
|
<ToolbarButton
|
||||||
content={
|
icon="FreezeAll"
|
||||||
<ShortcutDisplay
|
label="Freeze Path"
|
||||||
{...shortcuts.find(
|
onClick={() => {
|
||||||
({ name }) => name.toLowerCase() === "freeze path",
|
|
||||||
)!}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
side="top"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={cn(
|
|
||||||
"node-toolbar-buttons",
|
|
||||||
frozen && "text-blue-500",
|
|
||||||
)}
|
|
||||||
variant="ghost"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
FreezeAllVertices({
|
FreezeAllVertices({
|
||||||
flowId: currentFlowId,
|
flowId: currentFlowId,
|
||||||
stopNodeId: data.id,
|
stopNodeId: data.id,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
size="node-toolbar"
|
shortcut={shortcuts.find((s) =>
|
||||||
>
|
s.name.toLowerCase().startsWith("freeze path"),
|
||||||
<IconComponent
|
|
||||||
name="FreezeAll"
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 transition-all",
|
|
||||||
frozen ? "animate-wiggle text-ice" : "",
|
|
||||||
)}
|
)}
|
||||||
|
className={cn("node-toolbar-buttons", frozen && "text-blue-500")}
|
||||||
/>
|
/>
|
||||||
<span className="text-[13px] font-medium">Freeze Path</span>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
)}
|
)}
|
||||||
{hasToolMode && (
|
{hasToolMode && (
|
||||||
<ShadTooltip
|
<ToolbarButton
|
||||||
content={
|
icon="Hammer"
|
||||||
<ShortcutDisplay
|
label="Tool Mode"
|
||||||
{...shortcuts.find(
|
onClick={() => {
|
||||||
({ name }) => name.toLowerCase() === "tool mode",
|
takeSnapshot();
|
||||||
)!}
|
handleSelectChange("toolMode");
|
||||||
/>
|
}}
|
||||||
}
|
shortcut={shortcuts.find((s) =>
|
||||||
side="top"
|
s.name.toLowerCase().startsWith("tool mode"),
|
||||||
>
|
)}
|
||||||
<Button
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"node-toolbar-buttons h-[2rem]",
|
"node-toolbar-buttons h-[2rem]",
|
||||||
toolMode && "text-primary",
|
toolMode && "text-primary",
|
||||||
)}
|
)}
|
||||||
variant="ghost"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
takeSnapshot();
|
|
||||||
handleSelectChange("toolMode");
|
|
||||||
}}
|
|
||||||
size="node-toolbar"
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
name="Hammer"
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 transition-all",
|
|
||||||
toolMode ? "text-primary" : "",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<span className="text-[13px] font-medium">Tool Mode</span>
|
|
||||||
<ToggleShadComponent
|
|
||||||
value={toolMode}
|
|
||||||
editNode={false}
|
|
||||||
handleOnNewValue={() => {}}
|
|
||||||
disabled={false}
|
|
||||||
size="medium"
|
|
||||||
showToogle={false}
|
|
||||||
id="tool-mode-toggle"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
hasCode,
|
||||||
|
nodeLength,
|
||||||
|
hasToolMode,
|
||||||
|
toolMode,
|
||||||
|
data.id,
|
||||||
|
takeSnapshot,
|
||||||
|
FreezeAllVertices,
|
||||||
|
currentFlowId,
|
||||||
|
shortcuts,
|
||||||
|
frozen,
|
||||||
|
handleSelectChange,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="noflow nopan nodelete nodrag">
|
||||||
|
<div className="toolbar-wrapper">
|
||||||
|
{renderToolbarButtons}
|
||||||
<Select
|
<Select
|
||||||
onValueChange={handleSelectChange}
|
onValueChange={handleSelectChange}
|
||||||
value={selectedValue!}
|
value={selectedValue!}
|
||||||
|
|
@ -572,7 +521,10 @@ export default function NodeToolbarComponent({
|
||||||
size="node-toolbar"
|
size="node-toolbar"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<IconComponent name="MoreHorizontal" className="h-4 w-4" />
|
<IconComponent
|
||||||
|
name="MoreHorizontal"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
|
|
@ -593,11 +545,14 @@ export default function NodeToolbarComponent({
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
{nodeLength > 0 && (
|
{nodeLength > 0 && (
|
||||||
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
|
<SelectItem
|
||||||
|
value={nodeLength === 0 ? "disabled" : "advanced"}
|
||||||
|
>
|
||||||
<ToolbarSelectItem
|
<ToolbarSelectItem
|
||||||
shortcut={
|
shortcut={
|
||||||
shortcuts.find((obj) => obj.name === "Advanced Settings")
|
shortcuts.find(
|
||||||
?.shortcut!
|
(obj) => obj.name === "Advanced Settings",
|
||||||
|
)?.shortcut!
|
||||||
}
|
}
|
||||||
value={"Controls"}
|
value={"Controls"}
|
||||||
icon={"SlidersHorizontal"}
|
icon={"SlidersHorizontal"}
|
||||||
|
|
@ -619,7 +574,8 @@ export default function NodeToolbarComponent({
|
||||||
<SelectItem value={"duplicate"}>
|
<SelectItem value={"duplicate"}>
|
||||||
<ToolbarSelectItem
|
<ToolbarSelectItem
|
||||||
shortcut={
|
shortcut={
|
||||||
shortcuts.find((obj) => obj.name === "Duplicate")?.shortcut!
|
shortcuts.find((obj) => obj.name === "Duplicate")
|
||||||
|
?.shortcut!
|
||||||
}
|
}
|
||||||
value={"Duplicate"}
|
value={"Duplicate"}
|
||||||
icon={"Copy"}
|
icon={"Copy"}
|
||||||
|
|
@ -640,7 +596,8 @@ export default function NodeToolbarComponent({
|
||||||
<SelectItem value={"update"}>
|
<SelectItem value={"update"}>
|
||||||
<ToolbarSelectItem
|
<ToolbarSelectItem
|
||||||
shortcut={
|
shortcut={
|
||||||
shortcuts.find((obj) => obj.name === "Update")?.shortcut!
|
shortcuts.find((obj) => obj.name === "Update")
|
||||||
|
?.shortcut!
|
||||||
}
|
}
|
||||||
value={"Restore"}
|
value={"Restore"}
|
||||||
icon={"RefreshCcwDot"}
|
icon={"RefreshCcwDot"}
|
||||||
|
|
@ -732,7 +689,8 @@ export default function NodeToolbarComponent({
|
||||||
<SelectItem value="Download">
|
<SelectItem value="Download">
|
||||||
<ToolbarSelectItem
|
<ToolbarSelectItem
|
||||||
shortcut={
|
shortcut={
|
||||||
shortcuts.find((obj) => obj.name === "Download")?.shortcut!
|
shortcuts.find((obj) => obj.name === "Download")
|
||||||
|
?.shortcut!
|
||||||
}
|
}
|
||||||
value={"Download"}
|
value={"Download"}
|
||||||
icon={"Download"}
|
icon={"Download"}
|
||||||
|
|
@ -774,62 +732,31 @@ export default function NodeToolbarComponent({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ToolbarModals
|
||||||
open={showOverrideModal}
|
showModalAdvanced={showModalAdvanced}
|
||||||
title="Replace"
|
showconfirmShare={showconfirmShare}
|
||||||
onConfirm={handleConfirm}
|
showOverrideModal={showOverrideModal}
|
||||||
onClose={handleClose}
|
openModal={openModal}
|
||||||
onCancel={handleCancel}
|
hasCode={hasCode}
|
||||||
cancelText="Create New"
|
setShowModalAdvanced={setShowModalAdvanced}
|
||||||
confirmationText="Replace"
|
setShowconfirmShare={setShowconfirmShare}
|
||||||
size={"x-small"}
|
setShowOverrideModal={setShowOverrideModal}
|
||||||
icon={"SaveAll"}
|
setOpenModal={setOpenModal}
|
||||||
index={6}
|
|
||||||
>
|
|
||||||
<ConfirmationModal.Content>
|
|
||||||
<span>
|
|
||||||
It seems {data.node?.display_name} already exists. Do you want to
|
|
||||||
replace it with the current or create a new one?
|
|
||||||
</span>
|
|
||||||
</ConfirmationModal.Content>
|
|
||||||
</ConfirmationModal>
|
|
||||||
{showModalAdvanced && (
|
|
||||||
<EditNodeModal
|
|
||||||
data={data}
|
data={data}
|
||||||
open={showModalAdvanced}
|
flowComponent={flowComponent}
|
||||||
setOpen={setShowModalAdvanced}
|
handleOnNewValue={handleOnNewValue}
|
||||||
|
handleNodeClass={handleNodeClass}
|
||||||
|
setToolMode={setToolMode}
|
||||||
|
setSuccessData={setSuccessData}
|
||||||
|
addFlow={addFlow}
|
||||||
|
name={name}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{showconfirmShare && (
|
|
||||||
<ShareModal
|
|
||||||
open={showconfirmShare}
|
|
||||||
setOpen={setShowconfirmShare}
|
|
||||||
is_component={true}
|
|
||||||
component={flowComponent!}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasCode && (
|
|
||||||
<div className="hidden">
|
|
||||||
{openModal && (
|
|
||||||
<CodeAreaModal
|
|
||||||
setValue={handleOnNewValue}
|
|
||||||
open={openModal}
|
|
||||||
setOpen={setOpenModal}
|
|
||||||
dynamic={true}
|
|
||||||
setNodeClass={(apiClassType, type) => {
|
|
||||||
handleNodeClass(apiClassType, type);
|
|
||||||
setToolMode(false);
|
|
||||||
}}
|
|
||||||
nodeClass={data.node}
|
|
||||||
value={data.node?.template[name].value ?? ""}
|
|
||||||
componentId={data.id}
|
|
||||||
>
|
|
||||||
<></>
|
|
||||||
</CodeAreaModal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
NodeToolbarComponent.displayName = "NodeToolbarComponent";
|
||||||
|
|
||||||
|
export default NodeToolbarComponent;
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,8 @@ test(
|
||||||
.getByPlaceholder("Empty")
|
.getByPlaceholder("Empty")
|
||||||
.textContent();
|
.textContent();
|
||||||
|
|
||||||
|
await page.getByText("Close").last().click();
|
||||||
|
|
||||||
await page.getByTestId("btn-close-modal").click();
|
await page.getByTestId("btn-close-modal").click();
|
||||||
|
|
||||||
await page.getByTestId("textarea_str_input_value").first().fill(",");
|
await page.getByTestId("textarea_str_input_value").first().fill(",");
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ test(
|
||||||
|
|
||||||
await page.getByTestId("fit_view").click();
|
await page.getByTestId("fit_view").click();
|
||||||
|
|
||||||
expect(await page.getByText("Saved").isVisible()).toBeTruthy();
|
expect(await page.getByText("Saved").last().isVisible()).toBeTruthy();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByText("Saved")
|
.getByText("Saved")
|
||||||
|
|
@ -71,11 +71,19 @@ test(
|
||||||
|
|
||||||
await page.getByTestId("icon-ChevronLeft").last().click();
|
await page.getByTestId("icon-ChevronLeft").last().click();
|
||||||
|
|
||||||
await expect(
|
try {
|
||||||
page.getByText("Unsaved changes will be permanently lost."),
|
await page.waitForSelector(
|
||||||
).toBeVisible();
|
'text="Unsaved changes will be permanently lost."',
|
||||||
|
{
|
||||||
|
state: "visible",
|
||||||
|
timeout: 2000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await page.getByText("Exit Anyway", { exact: true }).click();
|
await page.getByText("Exit Anyway", { exact: true }).click();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Warning text not visible, skipping dialog confirmation");
|
||||||
|
}
|
||||||
|
|
||||||
await page.getByText("Untitled document").first().click();
|
await page.getByText("Untitled document").first().click();
|
||||||
|
|
||||||
|
|
@ -83,21 +91,34 @@ test(
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await page.getByText("NVIDIA").isVisible()).toBeFalsy();
|
const nvidiaNode = await page.getByTestId("div-generic-node").count();
|
||||||
|
expect(nvidiaNode).toBe(0);
|
||||||
|
|
||||||
await page.getByTestId("sidebar-search-input").click();
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
await page.getByTestId("sidebar-search-input").fill("NVIDIA");
|
await page.getByTestId("sidebar-search-input").fill("NVIDIA");
|
||||||
|
|
||||||
await page.waitForSelector('[data-testid="modelsNVIDIA"]', {
|
await page.keyboard.press("Escape");
|
||||||
timeout: 3000,
|
await page.locator('//*[@id="react-flow-id"]').click();
|
||||||
|
|
||||||
|
const lastNvidiaModel = page.getByTestId("modelsNVIDIA").last();
|
||||||
|
await lastNvidiaModel.scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await lastNvidiaModel.hover({ timeout: 5000 });
|
||||||
|
|
||||||
|
// Wait for the add component button to appear
|
||||||
|
await page.getByTestId("add-component-button-nvidia").waitFor({
|
||||||
|
state: "visible",
|
||||||
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
await page.getByTestId("add-component-button-nvidia").click();
|
||||||
.getByTestId("modelsNVIDIA")
|
} catch (error) {
|
||||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
console.error("Failed to hover or find add component button:", error);
|
||||||
await page.mouse.up();
|
throw error;
|
||||||
await page.mouse.down();
|
}
|
||||||
|
|
||||||
|
// Wait for fit view button
|
||||||
await page.waitForSelector('[data-testid="fit_view"]', {
|
await page.waitForSelector('[data-testid="fit_view"]', {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue