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 { cloneDeep } from "lodash";
|
||||
import { memo, useEffect, useMemo, useRef } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useUpdateNodeInternals } from "reactflow";
|
||||
import { default as IconComponent } from "../../../../components/common/genericIconComponent";
|
||||
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
||||
|
|
@ -22,7 +22,117 @@ import OutputComponent from "../OutputComponent";
|
|||
import HandleRenderComponent from "../handleRenderComponent";
|
||||
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,
|
||||
data,
|
||||
title,
|
||||
|
|
@ -39,89 +149,87 @@ export default function NodeOutputField({
|
|||
isToolMode = false,
|
||||
}: NodeOutputFieldComponentType): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
// Use selective store subscriptions
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const myData = useTypesStore((state) => state.data);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const myData = useTypesStore((state) => state.data);
|
||||
|
||||
let flowPoolId = data.id;
|
||||
let internalOutputName = outputName;
|
||||
|
||||
if (data.node?.flow && outputProxy) {
|
||||
const realOutput = getGroupOutputNodeId(
|
||||
data.node.flow,
|
||||
outputProxy.name,
|
||||
outputProxy.id,
|
||||
);
|
||||
if (realOutput) {
|
||||
flowPoolId = realOutput.id;
|
||||
internalOutputName = realOutput.outputName;
|
||||
// Memoize computed values
|
||||
const { flowPoolId, internalOutputName } = useMemo(() => {
|
||||
if (data.node?.flow && outputProxy) {
|
||||
const realOutput = getGroupOutputNodeId(
|
||||
data.node.flow,
|
||||
outputProxy.name,
|
||||
outputProxy.id,
|
||||
);
|
||||
if (realOutput) {
|
||||
return {
|
||||
flowPoolId: realOutput.id,
|
||||
internalOutputName: realOutput.outputName,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return { flowPoolId: data.id, internalOutputName: outputName };
|
||||
}, [data.id, data.node?.flow, outputProxy, outputName]);
|
||||
|
||||
const flowPoolNode = (flowPool[flowPoolId] ?? [])[
|
||||
(flowPool[flowPoolId]?.length ?? 1) - 1
|
||||
];
|
||||
const flowPoolNode = useMemo(() => {
|
||||
const pool = flowPool[flowPoolId] ?? [];
|
||||
return pool[pool.length - 1];
|
||||
}, [flowPool, flowPoolId]);
|
||||
|
||||
const displayOutputPreview =
|
||||
!!flowPool[flowPoolId] &&
|
||||
logHasMessage(flowPoolNode?.data, internalOutputName);
|
||||
|
||||
const unknownOutput = logTypeIsUnknown(
|
||||
flowPoolNode?.data,
|
||||
internalOutputName,
|
||||
const { displayOutputPreview, unknownOutput, errorOutput } = useMemo(
|
||||
() => ({
|
||||
displayOutputPreview:
|
||||
!!flowPool[flowPoolId] &&
|
||||
logHasMessage(flowPoolNode?.data, internalOutputName),
|
||||
unknownOutput: logTypeIsUnknown(flowPoolNode?.data, internalOutputName),
|
||||
errorOutput: logTypeIsError(flowPoolNode?.data, internalOutputName),
|
||||
}),
|
||||
[flowPool, flowPoolId, flowPoolNode?.data, internalOutputName],
|
||||
);
|
||||
const errorOutput = logTypeIsError(flowPoolNode?.data, internalOutputName);
|
||||
|
||||
let disabledOutput =
|
||||
edges.some((edge) => edge.sourceHandle === scapedJSONStringfy(id)) ?? false;
|
||||
const disabledOutput = useMemo(
|
||||
() => edges.some((edge) => edge.sourceHandle === scapedJSONStringfy(id)),
|
||||
[edges, id],
|
||||
);
|
||||
|
||||
const handleUpdateOutputHide = (value?: boolean) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: {
|
||||
...newNode.data.node,
|
||||
outputs: newNode.data.node.outputs?.map((output, i) => {
|
||||
if (i === index) {
|
||||
output.hidden = value ?? !output.hidden;
|
||||
}
|
||||
return output;
|
||||
}),
|
||||
},
|
||||
};
|
||||
return newNode;
|
||||
});
|
||||
updateNodeInternals(data.id);
|
||||
};
|
||||
const handleUpdateOutputHide = useCallback(
|
||||
(value?: boolean) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
const newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: {
|
||||
...newNode.data.node,
|
||||
outputs: newNode.data.node.outputs?.map((output, i) => {
|
||||
if (i === index) {
|
||||
output.hidden = value ?? !output.hidden;
|
||||
}
|
||||
return output;
|
||||
}),
|
||||
},
|
||||
};
|
||||
return newNode;
|
||||
});
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
[data.id, index, setNode, updateNodeInternals],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabledOutput && data.node?.outputs![index].hidden) {
|
||||
handleUpdateOutputHide(false);
|
||||
}
|
||||
}, [disabledOutput]);
|
||||
|
||||
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
|
||||
);
|
||||
},
|
||||
);
|
||||
}, [disabledOutput, data.node?.outputs, handleUpdateOutputHide, index]);
|
||||
|
||||
const Handle = useMemo(
|
||||
() => (
|
||||
<MemoizedHandleRenderComponent
|
||||
<HandleRenderComponent
|
||||
left={false}
|
||||
nodes={nodes}
|
||||
tooltipTitle={tooltipTitle}
|
||||
|
|
@ -153,9 +261,9 @@ export default function NodeOutputField({
|
|||
],
|
||||
);
|
||||
|
||||
return !showNode ? (
|
||||
<>{Handle}</>
|
||||
) : (
|
||||
if (!showNode) return <>{Handle}</>;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
|
|
@ -164,111 +272,75 @@ export default function NodeOutputField({
|
|||
isToolMode && "bg-primary",
|
||||
)}
|
||||
>
|
||||
<>
|
||||
<div className="flex w-full items-center justify-end truncate text-sm">
|
||||
<div className="flex flex-1">
|
||||
<Button
|
||||
disabled={disabledOutput}
|
||||
unstyled
|
||||
onClick={() => handleUpdateOutputHide()}
|
||||
data-testid={`input-inspection-${title.toLowerCase()}`}
|
||||
>
|
||||
<ShadTooltip
|
||||
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>
|
||||
</ShadTooltip>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{data.node?.frozen && (
|
||||
<div className="pr-1">
|
||||
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={data.node?.frozen ? "text-ice" : ""}>
|
||||
<OutputComponent
|
||||
proxy={outputProxy}
|
||||
idx={index}
|
||||
types={type?.split("|") ?? []}
|
||||
selected={
|
||||
data.node?.outputs![index].selected ??
|
||||
data.node?.outputs![index].types[0] ??
|
||||
title
|
||||
}
|
||||
nodeId={data.id}
|
||||
frozen={data.node?.frozen}
|
||||
name={title ?? type}
|
||||
isToolMode={isToolMode}
|
||||
/>
|
||||
</span>
|
||||
<ShadTooltip
|
||||
content={
|
||||
displayOutputPreview
|
||||
? unknownOutput
|
||||
? "Output can't be displayed"
|
||||
: "Inspect output"
|
||||
: "Please build the component first"
|
||||
}
|
||||
>
|
||||
<div className="flex">
|
||||
<OutputModal
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
nodeId={flowPoolId}
|
||||
outputName={internalOutputName}
|
||||
>
|
||||
<Button
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
data-testid={`output-inspection-${title.toLowerCase()}`}
|
||||
unstyled
|
||||
>
|
||||
{
|
||||
<IconComponent
|
||||
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" : "",
|
||||
)}
|
||||
name={"ScanEye"}
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
/>
|
||||
}
|
||||
</Button>
|
||||
</OutputModal>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end truncate text-sm">
|
||||
<div className="flex flex-1">
|
||||
<HideShowButton
|
||||
disabled={disabledOutput}
|
||||
onClick={() => handleUpdateOutputHide()}
|
||||
hidden={!!data.node?.outputs![index].hidden}
|
||||
isToolMode={isToolMode}
|
||||
title={title}
|
||||
/>
|
||||
</div>
|
||||
{Handle}
|
||||
</>
|
||||
|
||||
{data.node?.frozen && (
|
||||
<div className="pr-1">
|
||||
<SnowflakeIcon />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={data.node?.frozen ? "text-ice" : ""}>
|
||||
<MemoizedOutputComponent
|
||||
proxy={outputProxy}
|
||||
idx={index}
|
||||
types={type?.split("|") ?? []}
|
||||
selected={
|
||||
data.node?.outputs![index].selected ??
|
||||
data.node?.outputs![index].types[0] ??
|
||||
title
|
||||
}
|
||||
nodeId={data.id}
|
||||
frozen={data.node?.frozen}
|
||||
name={title ?? type}
|
||||
isToolMode={isToolMode}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<ShadTooltip
|
||||
content={
|
||||
displayOutputPreview
|
||||
? unknownOutput
|
||||
? "Output can't be displayed"
|
||||
: "Inspect output"
|
||||
: "Please build the component first"
|
||||
}
|
||||
>
|
||||
<div className="flex">
|
||||
<OutputModal
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
nodeId={flowPoolId}
|
||||
outputName={internalOutputName}
|
||||
>
|
||||
<InspectButton
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
displayOutputPreview={displayOutputPreview}
|
||||
unknownOutput={unknownOutput ?? false}
|
||||
errorOutput={errorOutput ?? false}
|
||||
isToolMode={isToolMode}
|
||||
title={title}
|
||||
onClick={() => {
|
||||
//just to trigger the memoization
|
||||
}}
|
||||
/>
|
||||
</OutputModal>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</div>
|
||||
{Handle}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NodeOutputField);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { getNodeInputColors } from "@/CustomNodes/helpers/get-node-input-colors";
|
||||
import { getNodeInputColorsName } from "@/CustomNodes/helpers/get-node-input-colors-name";
|
||||
import { sortToolModeFields } from "@/CustomNodes/helpers/sort-tool-mode-field";
|
||||
import getFieldTitle from "@/CustomNodes/utils/get-field-title";
|
||||
import { scapedJSONStringfy } from "@/utils/reactflowUtils";
|
||||
import { useMemo } from "react";
|
||||
import { sortToolModeFields } from "../..";
|
||||
import NodeInputField from "../NodeInputField";
|
||||
|
||||
const RenderInputParameters = ({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useDarkStore } from "@/stores/darkStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Handle, Position } from "reactflow";
|
||||
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
||||
import {
|
||||
|
|
@ -10,7 +10,147 @@ import {
|
|||
import { cn, groupByFamily } from "../../../../utils/utils";
|
||||
import HandleTooltipComponent from "../HandleTooltipComponent";
|
||||
|
||||
export default function HandleRenderComponent({
|
||||
const BASE_HANDLE_STYLES = {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
top: "50%",
|
||||
position: "absolute" as const,
|
||||
zIndex: 30,
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
} as const;
|
||||
|
||||
const HandleContent = memo(function HandleContent({
|
||||
isNullHandle,
|
||||
handleColor,
|
||||
accentForegroundColorName,
|
||||
isHovered,
|
||||
openHandle,
|
||||
testIdComplement,
|
||||
title,
|
||||
showNode,
|
||||
left,
|
||||
nodeId,
|
||||
colorName,
|
||||
}: {
|
||||
isNullHandle: boolean;
|
||||
handleColor: string;
|
||||
accentForegroundColorName: string;
|
||||
isHovered: boolean;
|
||||
openHandle: boolean;
|
||||
testIdComplement?: string;
|
||||
title: string;
|
||||
showNode: boolean;
|
||||
left: boolean;
|
||||
nodeId: string;
|
||||
colorName?: string[];
|
||||
}) {
|
||||
// Restore animation effect
|
||||
useEffect(() => {
|
||||
if ((isHovered || openHandle) && !isNullHandle) {
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.id = `pulse-${nodeId}`;
|
||||
styleSheet.textContent = `
|
||||
@keyframes pulseNeon {
|
||||
0% {
|
||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||
0 0 2px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 4px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 6px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 8px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 10px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 15px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 20px hsl(var(--datatype-${colorName?.[0]}));
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||
0 0 4px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 8px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 12px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 16px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 20px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 25px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 30px hsl(var(--datatype-${colorName?.[0]}));
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||
0 0 2px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 4px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 6px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 8px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 10px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 15px hsl(var(--datatype-${colorName?.[0]})),
|
||||
0 0 20px hsl(var(--datatype-${colorName?.[0]}));
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
return () => {
|
||||
const existingStyle = document.getElementById(`pulse-${nodeId}`);
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [isHovered, openHandle, isNullHandle, nodeId, colorName]);
|
||||
|
||||
const getNeonShadow = useCallback(
|
||||
(color: string, isActive: boolean) => {
|
||||
if (isNullHandle) return "none";
|
||||
if (!isActive) return `0 0 0 3px hsl(var(--${color}))`;
|
||||
return [
|
||||
"0 0 0 1px hsl(var(--border))",
|
||||
`0 0 2px ${color}`,
|
||||
`0 0 4px ${color}`,
|
||||
`0 0 6px ${color}`,
|
||||
`0 0 8px ${color}`,
|
||||
`0 0 10px ${color}`,
|
||||
`0 0 15px ${color}`,
|
||||
`0 0 20px ${color}`,
|
||||
].join(", ");
|
||||
},
|
||||
[isNullHandle],
|
||||
);
|
||||
|
||||
const contentStyle = useMemo(
|
||||
() => ({
|
||||
background: isNullHandle ? "hsl(var(--border))" : handleColor,
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
transition: "all 0.2s",
|
||||
boxShadow: getNeonShadow(
|
||||
accentForegroundColorName,
|
||||
isHovered || openHandle,
|
||||
),
|
||||
animation:
|
||||
(isHovered || openHandle) && !isNullHandle
|
||||
? "pulseNeon 1.1s ease-in-out infinite"
|
||||
: "none",
|
||||
border: isNullHandle ? "2px solid hsl(var(--muted))" : "none",
|
||||
}),
|
||||
[
|
||||
isNullHandle,
|
||||
handleColor,
|
||||
getNeonShadow,
|
||||
accentForegroundColorName,
|
||||
isHovered,
|
||||
openHandle,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<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,
|
||||
nodes,
|
||||
tooltipTitle = "",
|
||||
|
|
@ -35,236 +175,184 @@ export default function HandleRenderComponent({
|
|||
edges: any;
|
||||
myData: any;
|
||||
colors: string[];
|
||||
setFilterEdge: any;
|
||||
showNode: any;
|
||||
setFilterEdge: (edges: any) => void;
|
||||
showNode: boolean;
|
||||
testIdComplement?: string;
|
||||
nodeId: string;
|
||||
colorName?: string[];
|
||||
}) {
|
||||
const handleColorName = colorName?.[0] ?? "";
|
||||
|
||||
const accentColorName = `datatype-${handleColorName}`;
|
||||
const accentForegroundColorName = `${accentColorName}-foreground`;
|
||||
|
||||
const setHandleDragging = useFlowStore((state) => state.setHandleDragging);
|
||||
const setFilterType = useFlowStore((state) => state.setFilterType);
|
||||
const handleDragging = useFlowStore((state) => state.handleDragging);
|
||||
const filterType = useFlowStore((state) => state.filterType);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [openTooltip, setOpenTooltip] = useState(false);
|
||||
|
||||
const {
|
||||
setHandleDragging,
|
||||
setFilterType,
|
||||
handleDragging,
|
||||
filterType,
|
||||
onConnect,
|
||||
} = useFlowStore(
|
||||
useCallback(
|
||||
(state) => ({
|
||||
setHandleDragging: state.setHandleDragging,
|
||||
setFilterType: state.setFilterType,
|
||||
handleDragging: state.handleDragging,
|
||||
filterType: state.filterType,
|
||||
onConnect: state.onConnect,
|
||||
}),
|
||||
[],
|
||||
),
|
||||
);
|
||||
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
|
||||
const onConnect = useFlowStore((state) => state.onConnect);
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setHandleDragging(undefined);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
const myId = useMemo(
|
||||
() => scapedJSONStringfy(proxy ? { ...id, proxy } : id),
|
||||
[id, proxy],
|
||||
);
|
||||
|
||||
const getConnection = useMemo(
|
||||
() =>
|
||||
(semiConnection: {
|
||||
source: string | undefined;
|
||||
sourceHandle: string | undefined;
|
||||
target: string | undefined;
|
||||
targetHandle: string | undefined;
|
||||
}) => ({
|
||||
source: semiConnection.source ?? nodeId,
|
||||
sourceHandle: semiConnection.sourceHandle ?? myId,
|
||||
target: semiConnection.target ?? nodeId,
|
||||
targetHandle: semiConnection.targetHandle ?? myId,
|
||||
}),
|
||||
const getConnection = useCallback(
|
||||
(semiConnection: {
|
||||
source?: string;
|
||||
sourceHandle?: string;
|
||||
target?: string;
|
||||
targetHandle?: string;
|
||||
}) => ({
|
||||
source: semiConnection.source ?? nodeId,
|
||||
sourceHandle: semiConnection.sourceHandle ?? myId,
|
||||
target: semiConnection.target ?? nodeId,
|
||||
targetHandle: semiConnection.targetHandle ?? myId,
|
||||
}),
|
||||
[nodeId, myId],
|
||||
);
|
||||
|
||||
const sameDraggingNode = useMemo(
|
||||
() => (!left ? handleDragging?.target : handleDragging?.source) === nodeId,
|
||||
[left, handleDragging, nodeId],
|
||||
);
|
||||
const {
|
||||
sameNode,
|
||||
ownHandle,
|
||||
openHandle,
|
||||
filterOpenHandle,
|
||||
filterPresent,
|
||||
currentFilter,
|
||||
isNullHandle,
|
||||
handleColor,
|
||||
} = useMemo(() => {
|
||||
const sameDraggingNode =
|
||||
(!left ? handleDragging?.target : handleDragging?.source) === nodeId;
|
||||
const sameFilterNode =
|
||||
(!left ? filterType?.target : filterType?.source) === nodeId;
|
||||
|
||||
const ownDraggingHandle = useMemo(
|
||||
() =>
|
||||
const ownDraggingHandle =
|
||||
handleDragging &&
|
||||
(left ? handleDragging?.target : handleDragging?.source) &&
|
||||
(left ? handleDragging.targetHandle : handleDragging.sourceHandle) ===
|
||||
myId,
|
||||
[handleDragging, left, myId],
|
||||
);
|
||||
myId;
|
||||
|
||||
const sameFilterNode = useMemo(
|
||||
() => (!left ? filterType?.target : filterType?.source) === nodeId,
|
||||
[left, filterType, nodeId],
|
||||
);
|
||||
|
||||
const ownFilterHandle = useMemo(
|
||||
() =>
|
||||
const ownFilterHandle =
|
||||
filterType &&
|
||||
(left ? filterType?.target : filterType?.source) === nodeId &&
|
||||
(left ? filterType.targetHandle : filterType.sourceHandle) === myId,
|
||||
[filterType, left, myId],
|
||||
);
|
||||
(left ? filterType.targetHandle : filterType.sourceHandle) === myId;
|
||||
|
||||
const sameNode = useMemo(
|
||||
() => sameDraggingNode || sameFilterNode,
|
||||
[sameDraggingNode, sameFilterNode],
|
||||
);
|
||||
const ownHandle = useMemo(
|
||||
() => ownDraggingHandle || ownFilterHandle,
|
||||
[ownDraggingHandle, ownFilterHandle],
|
||||
);
|
||||
|
||||
const draggingOpenHandle = useMemo(
|
||||
() =>
|
||||
const draggingOpenHandle =
|
||||
handleDragging &&
|
||||
(left ? handleDragging.source : handleDragging.target) &&
|
||||
!ownDraggingHandle
|
||||
? isValidConnection(getConnection(handleDragging), nodes, edges)
|
||||
: false,
|
||||
[handleDragging, left, ownDraggingHandle, getConnection, nodes, edges],
|
||||
);
|
||||
: false;
|
||||
|
||||
const filterOpenHandle = useMemo(
|
||||
() =>
|
||||
const filterOpenHandle =
|
||||
filterType &&
|
||||
(left ? filterType.source : filterType.target) &&
|
||||
!ownFilterHandle
|
||||
? isValidConnection(getConnection(filterType), nodes, edges)
|
||||
: false,
|
||||
[filterType, left, ownFilterHandle, getConnection, nodes, edges],
|
||||
);
|
||||
: false;
|
||||
|
||||
const openHandle = useMemo(
|
||||
() => filterOpenHandle || draggingOpenHandle,
|
||||
[filterOpenHandle, draggingOpenHandle],
|
||||
);
|
||||
const openHandle = filterOpenHandle || draggingOpenHandle;
|
||||
const filterPresent = handleDragging || filterType;
|
||||
|
||||
const filterPresent = useMemo(
|
||||
() => handleDragging || filterType,
|
||||
[handleDragging, filterType],
|
||||
);
|
||||
|
||||
const currentFilter = useMemo(
|
||||
() =>
|
||||
left
|
||||
? {
|
||||
targetHandle: myId,
|
||||
target: nodeId,
|
||||
source: undefined,
|
||||
sourceHandle: undefined,
|
||||
type: tooltipTitle,
|
||||
color: handleColorName,
|
||||
}
|
||||
: {
|
||||
sourceHandle: myId,
|
||||
source: nodeId,
|
||||
target: undefined,
|
||||
targetHandle: undefined,
|
||||
type: tooltipTitle,
|
||||
color: handleColorName,
|
||||
},
|
||||
[left, myId, nodeId, tooltipTitle, colors],
|
||||
);
|
||||
|
||||
const isNullHandle = filterPresent && !(openHandle || ownHandle);
|
||||
|
||||
const handleColor = useMemo(
|
||||
() =>
|
||||
isNullHandle
|
||||
? dark
|
||||
? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)"
|
||||
: "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)"
|
||||
: "conic-gradient(" +
|
||||
colorName!
|
||||
.concat(colorName![0])
|
||||
.map(
|
||||
(color, index) =>
|
||||
`hsl(var(--datatype-${color}))` +
|
||||
" " +
|
||||
((360 / colors.length) * index - 360 / (colors.length * 4)) +
|
||||
"deg " +
|
||||
((360 / colors.length) * index + 360 / (colors.length * 4)) +
|
||||
"deg",
|
||||
)
|
||||
.join(" ,") +
|
||||
")",
|
||||
[filterPresent, openHandle, ownHandle, dark, colors],
|
||||
);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [openTooltip, setOpenTooltip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if ((isHovered || openHandle) && !isNullHandle) {
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.id = `pulse-${nodeId}`;
|
||||
styleSheet.textContent = `
|
||||
@keyframes pulseNeon {
|
||||
0% {
|
||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||
0 0 2px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 4px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 6px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 8px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 10px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 15px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 20px hsl(var(--datatype-${colorName![0]}));
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||
0 0 4px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 8px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 12px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 16px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 20px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 25px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 30px hsl(var(--datatype-${colorName![0]}));
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 2px hsl(var(--node-ring)),
|
||||
0 0 2px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 4px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 6px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 8px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 10px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 15px hsl(var(--datatype-${colorName![0]})),
|
||||
0 0 20px hsl(var(--datatype-${colorName![0]}));
|
||||
}
|
||||
const currentFilter = left
|
||||
? {
|
||||
targetHandle: myId,
|
||||
target: nodeId,
|
||||
source: undefined,
|
||||
sourceHandle: undefined,
|
||||
type: tooltipTitle,
|
||||
color: handleColorName,
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
: {
|
||||
sourceHandle: myId,
|
||||
source: nodeId,
|
||||
target: undefined,
|
||||
targetHandle: undefined,
|
||||
type: tooltipTitle,
|
||||
color: handleColorName,
|
||||
};
|
||||
|
||||
// Cleanup function should always be returned
|
||||
return () => {
|
||||
const existingStyle = document.getElementById(`pulse-${nodeId}`);
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
const isNullHandle =
|
||||
filterPresent && !(openHandle || ownDraggingHandle || ownFilterHandle);
|
||||
|
||||
const handleColor = isNullHandle
|
||||
? dark
|
||||
? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)"
|
||||
: "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)"
|
||||
: "conic-gradient(" +
|
||||
colorName!
|
||||
.concat(colorName![0])
|
||||
.map(
|
||||
(color, index) =>
|
||||
`hsl(var(--datatype-${color}))` +
|
||||
" " +
|
||||
((360 / colors.length) * index - 360 / (colors.length * 4)) +
|
||||
"deg " +
|
||||
((360 / colors.length) * index + 360 / (colors.length * 4)) +
|
||||
"deg",
|
||||
)
|
||||
.join(" ,") +
|
||||
")";
|
||||
|
||||
return {
|
||||
sameNode: sameDraggingNode || sameFilterNode,
|
||||
ownHandle: ownDraggingHandle || ownFilterHandle,
|
||||
openHandle,
|
||||
filterOpenHandle,
|
||||
filterPresent,
|
||||
currentFilter,
|
||||
isNullHandle,
|
||||
handleColor,
|
||||
};
|
||||
}, [isHovered, openHandle, isNullHandle, colors, nodeId]);
|
||||
}, [
|
||||
left,
|
||||
handleDragging,
|
||||
filterType,
|
||||
nodeId,
|
||||
myId,
|
||||
nodes,
|
||||
edges,
|
||||
getConnection,
|
||||
dark,
|
||||
colors,
|
||||
colorName,
|
||||
tooltipTitle,
|
||||
handleColorName,
|
||||
]);
|
||||
|
||||
const getNeonShadow = (color: string, isHovered: boolean) => {
|
||||
if (isNullHandle) return "none";
|
||||
if (!isHovered && !openHandle) return `0 0 0 3px hsl(var(--${color}))`;
|
||||
return [
|
||||
"0 0 0 1px hsl(var(--border))",
|
||||
`0 0 2px ${color}`,
|
||||
`0 0 4px ${color}`,
|
||||
`0 0 6px ${color}`,
|
||||
`0 0 8px ${color}`,
|
||||
`0 0 10px ${color}`,
|
||||
`0 0 15px ${color}`,
|
||||
`0 0 20px ${color}`,
|
||||
].join(", ");
|
||||
};
|
||||
const handleMouseDown = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (event.button === 0) {
|
||||
setHandleDragging(currentFilter);
|
||||
const handleMouseUp = () => {
|
||||
setHandleDragging(undefined);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
},
|
||||
[currentFilter, setHandleDragging],
|
||||
);
|
||||
|
||||
const handleRef = useRef<HTMLDivElement>(null);
|
||||
const invisibleDivRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClick = () => {
|
||||
const handleClick = useCallback(() => {
|
||||
setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!));
|
||||
setFilterType(currentFilter);
|
||||
if (filterOpenHandle && filterType) {
|
||||
|
|
@ -272,14 +360,40 @@ export default function HandleRenderComponent({
|
|||
setFilterType(undefined);
|
||||
setFilterEdge([]);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
myData,
|
||||
tooltipTitle,
|
||||
left,
|
||||
nodes,
|
||||
setFilterEdge,
|
||||
setFilterType,
|
||||
currentFilter,
|
||||
filterOpenHandle,
|
||||
filterType,
|
||||
onConnect,
|
||||
getConnection,
|
||||
]);
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsHovered(true), []);
|
||||
const handleMouseLeave = useCallback(() => setIsHovered(false), []);
|
||||
const handleMouseUp = useCallback(() => setOpenTooltip(false), []);
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => e.preventDefault(),
|
||||
[],
|
||||
);
|
||||
|
||||
// Memoize the validation function
|
||||
const validateConnection = useCallback(
|
||||
(connection: any) => isValidConnection(connection, nodes, edges),
|
||||
[nodes, edges],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ShadTooltip
|
||||
open={openTooltip}
|
||||
setOpen={setOpenTooltip}
|
||||
styleClasses={cn("tooltip-fixed-width custom-scroll nowheel bottom-2 ")}
|
||||
styleClasses={cn("tooltip-fixed-width custom-scroll nowheel bottom-2")}
|
||||
delayDuration={1000}
|
||||
content={
|
||||
<HandleTooltipComponent
|
||||
|
|
@ -296,75 +410,42 @@ export default function HandleRenderComponent({
|
|||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
ref={handleRef}
|
||||
data-testid={`handle-${testIdComplement}-${title.toLowerCase()}-${
|
||||
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
||||
}`}
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
key={myId}
|
||||
id={myId}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, nodes, edges)
|
||||
}
|
||||
isValidConnection={validateConnection}
|
||||
className={cn(
|
||||
`group/handle z-50 transition-all`,
|
||||
!showNode && "no-show",
|
||||
)}
|
||||
style={BASE_HANDLE_STYLES}
|
||||
onClick={handleClick}
|
||||
onMouseUp={() => {
|
||||
setOpenTooltip(false);
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === 0) {
|
||||
setHandleDragging(currentFilter);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
top: "50%",
|
||||
position: "absolute",
|
||||
zIndex: 30,
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onMouseUp={handleMouseUp}
|
||||
onContextMenu={handleContextMenu}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
data-testid={`handle-${testIdComplement}-${title.toLowerCase()}-${
|
||||
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
data-testid={`div-handle-${testIdComplement}-${title.toLowerCase()}-${
|
||||
!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"
|
||||
style={{
|
||||
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",
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
<HandleContent
|
||||
isNullHandle={isNullHandle ?? false}
|
||||
handleColor={handleColor}
|
||||
accentForegroundColorName={accentForegroundColorName}
|
||||
isHovered={isHovered}
|
||||
openHandle={openHandle}
|
||||
testIdComplement={testIdComplement}
|
||||
title={title}
|
||||
showNode={showNode}
|
||||
left={left}
|
||||
nodeId={nodeId}
|
||||
colorName={colorName}
|
||||
/>
|
||||
</Handle>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default HandleRenderComponent;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useUpdateNodeInternals } from "reactflow";
|
||||
import { Button } from "../../components/ui/button";
|
||||
|
|
@ -15,7 +15,7 @@ import useFlowStore from "../../stores/flowStore";
|
|||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useShortcutsStore } from "../../stores/shortcuts";
|
||||
import { useTypesStore } from "../../stores/typesStore";
|
||||
import { OutputFieldType, VertexBuildTypeAPI } from "../../types/api";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { checkHasToolMode } from "../../utils/reactflowUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
|
|
@ -23,7 +23,6 @@ import { classNames, cn } from "../../utils/utils";
|
|||
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
|
||||
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
||||
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||
import sortFields from "../utils/sort-fields";
|
||||
import NodeDescription from "./components/NodeDescription";
|
||||
import NodeName from "./components/NodeName";
|
||||
import { OutputParameter } from "./components/NodeOutputParameter";
|
||||
|
|
@ -32,27 +31,36 @@ import RenderInputParameters from "./components/RenderInputParameters";
|
|||
import { NodeIcon } from "./components/nodeIcon";
|
||||
import { useBuildStatus } from "./hooks/use-get-build-status";
|
||||
|
||||
export const sortToolModeFields = (
|
||||
a: string,
|
||||
b: string,
|
||||
template: any,
|
||||
fieldOrder: string[],
|
||||
isToolMode: boolean,
|
||||
) => {
|
||||
if (!isToolMode) return sortFields(a, b, fieldOrder);
|
||||
const MemoizedOutputParameter = memo(OutputParameter);
|
||||
const MemoizedRenderInputParameters = memo(RenderInputParameters);
|
||||
const MemoizedNodeIcon = memo(NodeIcon);
|
||||
const MemoizedNodeName = memo(NodeName);
|
||||
const MemoizedNodeStatus = memo(NodeStatus);
|
||||
const MemoizedNodeDescription = memo(NodeDescription);
|
||||
|
||||
const aToolMode = template[a]?.tool_mode ?? false;
|
||||
const bToolMode = template[b]?.tool_mode ?? false;
|
||||
const HiddenOutputsButton = memo(
|
||||
({
|
||||
showHiddenOutputs,
|
||||
onClick,
|
||||
}: {
|
||||
showHiddenOutputs: boolean;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<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
|
||||
if (aToolMode && !bToolMode) return 1;
|
||||
if (!aToolMode && bToolMode) return -1;
|
||||
|
||||
// If both are tool_mode or both aren't, use regular field order
|
||||
return sortFields(a, b, fieldOrder);
|
||||
};
|
||||
|
||||
export default function GenericNode({
|
||||
function GenericNode({
|
||||
data,
|
||||
selected,
|
||||
}: {
|
||||
|
|
@ -61,6 +69,14 @@ export default function GenericNode({
|
|||
xPos?: number;
|
||||
yPos?: number;
|
||||
}): JSX.Element {
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const [isUserEdited, setIsUserEdited] = useState(false);
|
||||
const [borderColor, setBorderColor] = useState<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 templates = useTypesStore((state) => state.templates);
|
||||
const deleteNode = useFlowStore((state) => state.deleteNode);
|
||||
|
|
@ -68,11 +84,19 @@ export default function GenericNode({
|
|||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const [isUserEdited, setIsUserEdited] = useState(false);
|
||||
const [borderColor, setBorderColor] = useState<string>("");
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||
const buildStatus = useBuildStatus(data, data.id);
|
||||
|
||||
const showNode = data.showNode ?? true;
|
||||
|
||||
const getValidationStatus = (data) => {
|
||||
setValidationStatus(data);
|
||||
return null;
|
||||
};
|
||||
|
||||
const { mutate: validateComponentCode } = usePostValidateComponentCode();
|
||||
|
||||
const updateNodeCode = useUpdateNodeCode(
|
||||
data?.id,
|
||||
data.node!,
|
||||
|
|
@ -82,6 +106,8 @@ export default function GenericNode({
|
|||
updateNodeInternals,
|
||||
);
|
||||
|
||||
useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
|
|
@ -94,23 +120,11 @@ export default function GenericNode({
|
|||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
|
||||
|
||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||||
|
||||
const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
|
||||
|
||||
const { mutate: validateComponentCode } = usePostValidateComponentCode();
|
||||
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
|
||||
const handleUpdateCode = () => {
|
||||
const handleUpdateCode = useCallback(() => {
|
||||
setLoadingUpdate(true);
|
||||
takeSnapshot();
|
||||
// to update we must get the code from the templates in useTypesStore
|
||||
|
||||
const thisNodeTemplate = templates[data.type]?.template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate?.code) return;
|
||||
|
||||
const currentCode = thisNodeTemplate.code.value;
|
||||
|
|
@ -125,70 +139,93 @@ export default function GenericNode({
|
|||
edges,
|
||||
data.id,
|
||||
);
|
||||
|
||||
updateNodeCode(newNode, currentCode, "code", type);
|
||||
setLoadingUpdate(false);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrorData({
|
||||
title: "Error updating Compoenent code",
|
||||
title: "Error updating Component code",
|
||||
list: [
|
||||
"There was an error updating the Component.",
|
||||
"If the error persists, please report it on our Discord or GitHub.",
|
||||
],
|
||||
});
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
setLoadingUpdate(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
data,
|
||||
templates,
|
||||
edges,
|
||||
updateNodeCode,
|
||||
validateComponentCode,
|
||||
setErrorData,
|
||||
takeSnapshot,
|
||||
]);
|
||||
|
||||
function handleUpdateCodeWShortcut() {
|
||||
const handleUpdateCodeWShortcut = useCallback(() => {
|
||||
if (isOutdated && selected) {
|
||||
handleUpdateCode();
|
||||
}
|
||||
}
|
||||
|
||||
const shownOutputs =
|
||||
data.node!.outputs?.filter((output) => !output.hidden) ?? [];
|
||||
|
||||
const hiddenOutputs =
|
||||
data.node!.outputs?.filter((output) => output.hidden) ?? [];
|
||||
}, [isOutdated, selected, handleUpdateCode]);
|
||||
|
||||
const update = useShortcutsStore((state) => state.update);
|
||||
useHotkeys(update, handleUpdateCodeWShortcut, { preventDefault: true });
|
||||
|
||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||
// Memoized values
|
||||
const isToolMode = useMemo(
|
||||
() =>
|
||||
data.node?.outputs?.some(
|
||||
(output) => output.name === "component_as_tool",
|
||||
) ?? false,
|
||||
[data.node?.outputs],
|
||||
);
|
||||
|
||||
const [openShowMoreOptions, setOpenShowMoreOptions] = useState(false);
|
||||
const hasToolMode = useMemo(
|
||||
() => checkHasToolMode(data.node?.template ?? {}),
|
||||
[data.node?.template],
|
||||
);
|
||||
|
||||
const renderOutputs = (outputs) => {
|
||||
return outputs.map((output, idx) => (
|
||||
<OutputParameter
|
||||
key={output.name + idx}
|
||||
output={output}
|
||||
idx={
|
||||
data.node!.outputs?.findIndex((out) => out.name === output.name) ??
|
||||
idx
|
||||
}
|
||||
lastOutput={idx === outputs.length - 1}
|
||||
data={data}
|
||||
types={types}
|
||||
selected={selected}
|
||||
showNode={showNode}
|
||||
isToolMode={isToolMode}
|
||||
/>
|
||||
));
|
||||
};
|
||||
const hasOutputs = useMemo(
|
||||
() => data.node?.outputs && data.node.outputs.length > 0,
|
||||
[data.node?.outputs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hiddenOutputs && hiddenOutputs.length == 0) {
|
||||
setShowHiddenOutputs(false);
|
||||
}
|
||||
}, [hiddenOutputs]);
|
||||
const renderOutputs = useCallback(
|
||||
(outputs, key?: string) => {
|
||||
return outputs?.map((output, idx) => (
|
||||
<MemoizedOutputParameter
|
||||
key={`${key}-${output.name}-${idx}`}
|
||||
output={output}
|
||||
idx={
|
||||
data.node!.outputs?.findIndex((out) => out.name === output.name) ??
|
||||
idx
|
||||
}
|
||||
lastOutput={idx === outputs.length - 1}
|
||||
data={data}
|
||||
types={types}
|
||||
selected={selected}
|
||||
showNode={showNode}
|
||||
isToolMode={isToolMode}
|
||||
/>
|
||||
));
|
||||
},
|
||||
[data, types, selected, showNode, isToolMode],
|
||||
);
|
||||
|
||||
const { shownOutputs, hiddenOutputs } = useMemo(
|
||||
() => ({
|
||||
shownOutputs:
|
||||
data.node?.outputs?.filter((output) => !output.hidden) ?? [],
|
||||
hiddenOutputs:
|
||||
data.node?.outputs?.filter((output) => output.hidden) ?? [],
|
||||
}),
|
||||
[data.node?.outputs],
|
||||
);
|
||||
|
||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||
return selected ? (
|
||||
|
|
@ -211,7 +248,6 @@ export default function GenericNode({
|
|||
onCloseAdvancedModal={() => {}}
|
||||
updateNode={handleUpdateCode}
|
||||
isOutdated={isOutdated && isUserEdited}
|
||||
setOpenShowMoreOptions={setOpenShowMoreOptions}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -230,20 +266,95 @@ export default function GenericNode({
|
|||
shortcuts,
|
||||
]);
|
||||
|
||||
const isToolMode =
|
||||
data.node?.outputs?.some((output) => output.name === "component_as_tool") ??
|
||||
false;
|
||||
useEffect(() => {
|
||||
if (hiddenOutputs && hiddenOutputs.length === 0) {
|
||||
setShowHiddenOutputs(false);
|
||||
}
|
||||
}, [hiddenOutputs]);
|
||||
|
||||
const buildStatus = useBuildStatus(data, data.id);
|
||||
const hasOutputs = data.node?.outputs && data.node?.outputs.length > 0;
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<VertexBuildTypeAPI | null>(null);
|
||||
const getValidationStatus = (data) => {
|
||||
setValidationStatus(data);
|
||||
return null;
|
||||
};
|
||||
const renderNodeIcon = useCallback(() => {
|
||||
return (
|
||||
<MemoizedNodeIcon
|
||||
dataType={data.type}
|
||||
showNode={showNode}
|
||||
icon={data.node?.icon}
|
||||
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 (
|
||||
<div className={cn(isOutdated && !isUserEdited ? "relative -mt-10" : "")}>
|
||||
|
|
@ -298,78 +409,27 @@ export default function GenericNode({
|
|||
className={"generic-node-title-arrangement"}
|
||||
data-testid="generic-node-title-arrangement"
|
||||
>
|
||||
<NodeIcon
|
||||
dataType={data.type}
|
||||
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>
|
||||
{renderNodeIcon()}
|
||||
<div className="generic-node-tooltip-div">{renderNodeName()}</div>
|
||||
</div>
|
||||
<div>
|
||||
{!showNode && (
|
||||
<>
|
||||
<RenderInputParameters
|
||||
data={data}
|
||||
types={types}
|
||||
isToolMode={isToolMode}
|
||||
showNode={showNode}
|
||||
shownOutputs={shownOutputs}
|
||||
showHiddenOutputs={showHiddenOutputs}
|
||||
/>
|
||||
{renderInputParameters()}
|
||||
{shownOutputs &&
|
||||
shownOutputs.length > 0 &&
|
||||
renderOutputs(shownOutputs)}
|
||||
renderOutputs(shownOutputs, "render-outputs")}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<NodeStatus
|
||||
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}
|
||||
/>
|
||||
{renderNodeStatus()}
|
||||
</div>
|
||||
{showNode && (
|
||||
<div>
|
||||
<NodeDescription
|
||||
description={data.node?.description}
|
||||
mdClassName={"dark:prose-invert"}
|
||||
nodeId={data.id}
|
||||
selected={selected}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showNode && <div>{renderDescription()}</div>}
|
||||
</div>
|
||||
{showNode && (
|
||||
<div className="relative">
|
||||
<>
|
||||
<RenderInputParameters
|
||||
data={data}
|
||||
types={types}
|
||||
isToolMode={isToolMode}
|
||||
showNode={showNode}
|
||||
shownOutputs={shownOutputs}
|
||||
showHiddenOutputs={showHiddenOutputs}
|
||||
/>
|
||||
{renderInputParameters()}
|
||||
<div
|
||||
className={classNames(
|
||||
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
|
||||
|
|
@ -380,44 +440,13 @@ export default function GenericNode({
|
|||
</div>
|
||||
{!showHiddenOutputs &&
|
||||
shownOutputs &&
|
||||
shownOutputs.map((output, idx) => (
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
renderOutputs(shownOutputs, "shown")}
|
||||
|
||||
<div
|
||||
className={cn(showHiddenOutputs ? "" : "h-0 overflow-hidden")}
|
||||
>
|
||||
<div className="block">
|
||||
{data.node!.outputs?.map((output, idx) => (
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
{renderOutputs(data.node!.outputs, "hidden")}
|
||||
</div>
|
||||
</div>
|
||||
{hiddenOutputs && hiddenOutputs.length > 0 && (
|
||||
|
|
@ -437,17 +466,10 @@ export default function GenericNode({
|
|||
: "bottom-[-0.8rem]",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
unstyled
|
||||
className="group flex h-6 w-6 items-center justify-center rounded-full border bg-background hover:border-foreground hover:text-foreground"
|
||||
<HiddenOutputsButton
|
||||
showHiddenOutputs={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>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
|
|
@ -458,3 +480,5 @@ export default function GenericNode({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(GenericNode);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContentWithoutPortal,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select-custom";
|
||||
import { Select, SelectTrigger } from "@/components/ui/select-custom";
|
||||
import { COLOR_OPTIONS } from "@/constants/constants";
|
||||
import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
|
|
@ -20,9 +13,12 @@ import { useShortcutsStore } from "@/stores/shortcuts";
|
|||
import { noteDataType } from "@/types/flow";
|
||||
import { classNames, cn, openInNewTab } from "@/utils/utils";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { memo, useCallback, useMemo } from "react";
|
||||
import IconComponent from "../../../components/common/genericIconComponent";
|
||||
import { ColorPickerButtons } from "../components/color-picker-buttons";
|
||||
import { SelectItems } from "../components/select-items";
|
||||
|
||||
export default function NoteToolbarComponent({
|
||||
const NoteToolbarComponent = memo(function NoteToolbarComponent({
|
||||
data,
|
||||
bgColor,
|
||||
}: {
|
||||
|
|
@ -30,191 +26,142 @@ export default function NoteToolbarComponent({
|
|||
bgColor: string;
|
||||
}) {
|
||||
const setNoticeData = useAlertStore((state) => state.setNoticeData);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const setLastCopiedSelection = useFlowStore(
|
||||
(state) => state.setLastCopiedSelection,
|
||||
);
|
||||
const paste = useFlowStore((state) => state.paste);
|
||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||
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) {
|
||||
return openInNewTab(data.node?.documentation);
|
||||
}
|
||||
setNoticeData({
|
||||
title: `${data.id} docs is not available at the moment.`,
|
||||
});
|
||||
}
|
||||
}, [data.node?.documentation, data.id, setNoticeData]);
|
||||
|
||||
const handleSelectChange = (event) => {
|
||||
switch (event) {
|
||||
case "documentation":
|
||||
openDocs();
|
||||
break;
|
||||
case "delete":
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
break;
|
||||
case "copy":
|
||||
const node = nodes.filter((node) => node.id === data.id);
|
||||
setLastCopiedSelection({ nodes: cloneDeep(node), edges: [] });
|
||||
break;
|
||||
case "duplicate":
|
||||
paste(
|
||||
{
|
||||
nodes: [nodes.find((node) => node.id === data.id)!],
|
||||
edges: [],
|
||||
},
|
||||
{
|
||||
x: 50,
|
||||
y: 10,
|
||||
paneX: nodes.find((node) => node.id === data.id)?.position.x,
|
||||
paneY: nodes.find((node) => node.id === data.id)?.position.y,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
// the deafult value is allways the first one if none is provided
|
||||
return (
|
||||
<>
|
||||
<div className="w-26 noflow nowheel nopan nodelete nodrag h-10">
|
||||
<span className="isolate inline-flex rounded-md shadow-sm">
|
||||
<Popover>
|
||||
<ShadTooltip content="Pick Color">
|
||||
<PopoverTrigger>
|
||||
<div>
|
||||
<div
|
||||
data-testid="color_picker"
|
||||
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
|
||||
style={{
|
||||
backgroundColor: COLOR_OPTIONS[bgColor] ?? "#00000000",
|
||||
}}
|
||||
className={cn(
|
||||
"h-4 w-4 rounded-full",
|
||||
COLOR_OPTIONS[bgColor] === null && "border",
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
</ShadTooltip>
|
||||
<PopoverContent side="top" className="w-fit px-2 py-2">
|
||||
<div className="flew-row flex gap-3">
|
||||
{Object.entries(COLOR_OPTIONS).map(([color, code]) => {
|
||||
return (
|
||||
<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",
|
||||
}}
|
||||
></div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Select onValueChange={handleSelectChange} value="">
|
||||
<SelectTrigger>
|
||||
<ShadTooltip content="Show More" side="top">
|
||||
<div>
|
||||
<div
|
||||
data-testid="more-options-modal"
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex h-8 w-[2rem] items-center rounded-r-md bg-background text-foreground shadow-md transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
name="MoreHorizontal"
|
||||
className="relative left-2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</SelectTrigger>
|
||||
<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">
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
const handleSelectChange = useCallback(
|
||||
(event: string) => {
|
||||
switch (event) {
|
||||
case "documentation":
|
||||
openDocs();
|
||||
break;
|
||||
case "delete":
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
break;
|
||||
case "copy":
|
||||
const node = nodes.filter((node) => node.id === data.id);
|
||||
setLastCopiedSelection({ nodes: cloneDeep(node), edges: [] });
|
||||
break;
|
||||
case "duplicate":
|
||||
const targetNode = nodes.find((node) => node.id === data.id);
|
||||
if (targetNode) {
|
||||
paste(
|
||||
{
|
||||
nodes: [targetNode],
|
||||
edges: [],
|
||||
},
|
||||
{
|
||||
x: 50,
|
||||
y: 10,
|
||||
paneX: targetNode.position.x,
|
||||
paneY: targetNode.position.y,
|
||||
},
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
openDocs,
|
||||
takeSnapshot,
|
||||
deleteNode,
|
||||
data.id,
|
||||
nodes,
|
||||
setLastCopiedSelection,
|
||||
paste,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Memoize the color picker background style
|
||||
const colorPickerStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: COLOR_OPTIONS[bgColor] ?? "#00000000",
|
||||
}),
|
||||
[bgColor],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-26 noflow nowheel nopan nodelete nodrag h-10">
|
||||
<span className="isolate inline-flex rounded-md shadow-sm">
|
||||
<Popover>
|
||||
<ShadTooltip content="Pick Color">
|
||||
<PopoverTrigger>
|
||||
<div>
|
||||
<div
|
||||
data-testid="color_picker"
|
||||
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
|
||||
style={colorPickerStyle}
|
||||
className={cn(
|
||||
"h-4 w-4 rounded-full",
|
||||
COLOR_OPTIONS[bgColor] === null && "border",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
</ShadTooltip>
|
||||
<PopoverContent side="top" className="w-fit px-2 py-2">
|
||||
<ColorPickerButtons
|
||||
bgColor={bgColor}
|
||||
data={data}
|
||||
setNode={setNode}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Select onValueChange={handleSelectChange} value="">
|
||||
<SelectTrigger>
|
||||
<ShadTooltip content="Show More" side="top">
|
||||
<div>
|
||||
<div
|
||||
data-testid="more-options-modal"
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex h-8 w-[2rem] items-center rounded-r-md bg-background text-foreground shadow-md transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
name="MoreHorizontal"
|
||||
className="relative left-2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</SelectTrigger>
|
||||
<SelectItems shortcuts={shortcuts} data={data} />
|
||||
</Select>
|
||||
</span>
|
||||
</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 { NodeType } from "@/types/flow";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useUpdateNodeInternals } from "reactflow";
|
||||
import { mutateTemplate } from "../helpers/mutate-template";
|
||||
|
||||
|
|
@ -32,51 +33,31 @@ const useHandleOnNewValue = ({
|
|||
) => void;
|
||||
}) => {
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
const setNode = setNodeExternal ?? useFlowStore((state) => state.setNode);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
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?) => {
|
||||
const newNode = cloneDeep(node);
|
||||
const template = newNode.template;
|
||||
// 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],
|
||||
),
|
||||
);
|
||||
|
||||
track("Component Edited", { nodeId });
|
||||
|
||||
if (!template) {
|
||||
setErrorData({ title: "Template not found in the component" });
|
||||
return;
|
||||
}
|
||||
|
||||
const parameter = template[name];
|
||||
|
||||
if (!parameter) {
|
||||
setErrorData({ title: "Parameter not found in the template" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options?.skipSnapshot) takeSnapshot();
|
||||
|
||||
Object.entries(changes).forEach(([key, value]) => {
|
||||
if (value !== undefined) parameter[key] = value;
|
||||
});
|
||||
|
||||
const shouldUpdate = parameter.real_time_refresh;
|
||||
|
||||
const setNodeClass = (newNodeClass: APIClassType) => {
|
||||
options?.setNodeClass && options.setNodeClass(newNodeClass);
|
||||
// Memoize the node update function
|
||||
const updateNodeState = useCallback(
|
||||
(newNode: APIClassType) => {
|
||||
setNode(
|
||||
nodeId,
|
||||
(oldNode) => {
|
||||
const newData = cloneDeep(oldNode.data);
|
||||
newData.node = newNodeClass;
|
||||
newData.node = newNode;
|
||||
return {
|
||||
...oldNode,
|
||||
data: newData,
|
||||
|
|
@ -87,34 +68,66 @@ const useHandleOnNewValue = ({
|
|||
updateNodeInternals(nodeId);
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
[nodeId, setNode, updateNodeInternals],
|
||||
);
|
||||
|
||||
if (shouldUpdate && changes.value !== undefined) {
|
||||
mutateTemplate(
|
||||
changes.value,
|
||||
newNode,
|
||||
setNodeClass,
|
||||
postTemplateValue,
|
||||
setErrorData,
|
||||
);
|
||||
}
|
||||
// Memoize the handleOnNewValue function
|
||||
const handleOnNewValue: handleOnNewValueType = useCallback(
|
||||
async (changes, options?) => {
|
||||
const newNode = cloneDeep(node);
|
||||
const template = newNode.template;
|
||||
|
||||
setNode(
|
||||
// Debounced tracking
|
||||
track("Component Edited", { nodeId });
|
||||
|
||||
if (!template) {
|
||||
setErrorData({ title: "Template not found in the component" });
|
||||
return;
|
||||
}
|
||||
|
||||
const parameter = template[name];
|
||||
|
||||
if (!parameter) {
|
||||
setErrorData({ title: "Parameter not found in the template" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options?.skipSnapshot) takeSnapshot();
|
||||
|
||||
Object.entries(changes).forEach(([key, value]) => {
|
||||
if (value !== undefined) parameter[key] = value;
|
||||
});
|
||||
|
||||
const shouldUpdate = parameter.real_time_refresh;
|
||||
|
||||
const setNodeClass = (newNodeClass: APIClassType) => {
|
||||
options?.setNodeClass?.(newNodeClass);
|
||||
updateNodeState(newNodeClass);
|
||||
};
|
||||
|
||||
if (shouldUpdate && changes.value !== undefined) {
|
||||
await mutateTemplate(
|
||||
changes.value,
|
||||
newNode,
|
||||
setNodeClass,
|
||||
postTemplateValue,
|
||||
setErrorData,
|
||||
);
|
||||
}
|
||||
|
||||
updateNodeState(newNode);
|
||||
},
|
||||
[
|
||||
node,
|
||||
nodeId,
|
||||
(oldNode) => {
|
||||
const newData = cloneDeep(oldNode.data);
|
||||
newData.node = newNode;
|
||||
return {
|
||||
...oldNode,
|
||||
data: newData,
|
||||
};
|
||||
},
|
||||
true,
|
||||
() => {
|
||||
updateNodeInternals(nodeId);
|
||||
},
|
||||
);
|
||||
};
|
||||
name,
|
||||
takeSnapshot,
|
||||
postTemplateValue,
|
||||
setErrorData,
|
||||
updateNodeState,
|
||||
],
|
||||
);
|
||||
|
||||
return { handleOnNewValue };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,54 +1,97 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, memo, useMemo } from "react";
|
||||
import { ShadToolTipType } from "../../../types/components";
|
||||
import { cn } from "../../../utils/utils";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
|
||||
|
||||
const ShadTooltip = forwardRef<HTMLDivElement, ShadToolTipType>(
|
||||
(
|
||||
{
|
||||
content,
|
||||
side,
|
||||
asChild = true,
|
||||
children,
|
||||
styleClasses,
|
||||
delayDuration = 500,
|
||||
open,
|
||||
align,
|
||||
setOpen,
|
||||
avoidCollisions = false,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
if (!content) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
// Extract static styles
|
||||
const BASE_TOOLTIP_CLASSES =
|
||||
"z-[99] max-w-96 bg-tooltip text-[12px] text-tooltip-foreground";
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
defaultOpen={!children}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
delayDuration={delayDuration}
|
||||
>
|
||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-[99] max-w-96 bg-tooltip text-[12px] text-tooltip-foreground",
|
||||
styleClasses,
|
||||
)}
|
||||
side={side}
|
||||
avoidCollisions={avoidCollisions}
|
||||
align={align}
|
||||
sticky="always"
|
||||
>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
// 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>
|
||||
)),
|
||||
);
|
||||
|
||||
ShadTooltip.displayName = "ShadTooltip";
|
||||
MemoizedTooltipContent.displayName = "MemoizedTooltipContent";
|
||||
|
||||
// Memoize the main tooltip component
|
||||
const ShadTooltip = memo(
|
||||
forwardRef<HTMLDivElement, ShadToolTipType>(
|
||||
(
|
||||
{
|
||||
content,
|
||||
side,
|
||||
asChild = true,
|
||||
children,
|
||||
styleClasses,
|
||||
delayDuration = 500,
|
||||
open,
|
||||
align,
|
||||
setOpen,
|
||||
avoidCollisions = false,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// Early return if no content
|
||||
if (!content) {
|
||||
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 (
|
||||
<Tooltip {...tooltipProps}>
|
||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
<MemoizedTooltipContent
|
||||
ref={ref}
|
||||
className={tooltipClassName}
|
||||
side={side}
|
||||
avoidCollisions={avoidCollisions}
|
||||
align={align}
|
||||
>
|
||||
{content}
|
||||
</MemoizedTooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Add display name for dev tools
|
||||
ShadTooltip.displayName = "ShadTooltip";
|
||||
export default ShadTooltip;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
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 CodeAreaComponent from "@/components/core/parameterRenderComponent/components/codeAreaComponent";
|
||||
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 { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
||||
import FloatComponent from "./components/floatComponent";
|
||||
|
|
@ -18,21 +21,24 @@ import PromptAreaComponent from "./components/promptComponent";
|
|||
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
||||
import { StrRenderComponent } from "./components/strRenderComponent";
|
||||
import ToggleShadComponent from "./components/toggleShadComponent";
|
||||
import { InputProps } from "./types";
|
||||
|
||||
export function ParameterRenderComponent({
|
||||
handleOnNewValue,
|
||||
name,
|
||||
nodeId,
|
||||
templateData,
|
||||
templateValue,
|
||||
editNode,
|
||||
handleNodeClass,
|
||||
nodeClass,
|
||||
disabled,
|
||||
placeholder,
|
||||
isToolMode,
|
||||
}: {
|
||||
const MemoizedTableNode = memo(TableNodeComponent);
|
||||
const MemoizedCodeArea = memo(CodeAreaComponent);
|
||||
const MemoizedSlider = memo(SliderComponent);
|
||||
const MemoizedDict = memo(DictComponent);
|
||||
const MemoizedEmpty = memo(EmptyParameterComponent);
|
||||
const MemoizedFloat = memo(FloatComponent);
|
||||
const MemoizedInputFile = memo(InputFileComponent);
|
||||
const MemoizedInputList = memo(InputListComponent);
|
||||
const MemoizedInt = memo(IntComponent);
|
||||
const MemoizedKeypairList = memo(KeypairListComponent);
|
||||
const MemoizedLink = memo(LinkComponent);
|
||||
const MemoizedMultiselect = memo(MultiselectComponent);
|
||||
const MemoizedPromptArea = memo(PromptAreaComponent);
|
||||
const MemoizedStrRender = memo(StrRenderComponent);
|
||||
const MemoizedToggleShad = memo(ToggleShadComponent);
|
||||
|
||||
interface ParameterRenderProps {
|
||||
handleOnNewValue: handleOnNewValueType;
|
||||
name: string;
|
||||
nodeId: string;
|
||||
|
|
@ -44,16 +50,34 @@ export function ParameterRenderComponent({
|
|||
disabled: boolean;
|
||||
placeholder?: string;
|
||||
isToolMode?: boolean;
|
||||
}) {
|
||||
const id = (
|
||||
templateData.type +
|
||||
"_" +
|
||||
(editNode ? "edit_" : "") +
|
||||
templateData.name
|
||||
).toLowerCase();
|
||||
}
|
||||
|
||||
const renderComponent = (): React.ReactElement<InputProps> => {
|
||||
const baseInputProps: InputProps = {
|
||||
export const ParameterRenderComponent = memo(function ParameterRenderComponent({
|
||||
handleOnNewValue,
|
||||
name,
|
||||
nodeId,
|
||||
templateData,
|
||||
templateValue,
|
||||
editNode,
|
||||
handleNodeClass,
|
||||
nodeClass,
|
||||
disabled,
|
||||
placeholder,
|
||||
isToolMode,
|
||||
}: ParameterRenderProps) {
|
||||
const id = useMemo(
|
||||
() =>
|
||||
(
|
||||
templateData.type +
|
||||
"_" +
|
||||
(editNode ? "edit_" : "") +
|
||||
templateData.name
|
||||
).toLowerCase(),
|
||||
[templateData.type, templateData.name, editNode],
|
||||
);
|
||||
|
||||
const baseInputProps = useMemo(
|
||||
() => ({
|
||||
id,
|
||||
value: templateValue,
|
||||
editNode,
|
||||
|
|
@ -64,12 +88,27 @@ export function ParameterRenderComponent({
|
|||
readonly: templateData.readonly,
|
||||
placeholder,
|
||||
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 (templateData.list) {
|
||||
if (!templateData.options) {
|
||||
return (
|
||||
<InputListComponent
|
||||
<MemoizedInputList
|
||||
{...baseInputProps}
|
||||
componentName={name}
|
||||
id={`inputlist_${id}`}
|
||||
|
|
@ -78,7 +117,7 @@ export function ParameterRenderComponent({
|
|||
}
|
||||
if (!!templateData.options) {
|
||||
return (
|
||||
<MultiselectComponent
|
||||
<MemoizedMultiselect
|
||||
{...baseInputProps}
|
||||
combobox={templateData.combobox}
|
||||
options={
|
||||
|
|
@ -92,7 +131,7 @@ export function ParameterRenderComponent({
|
|||
}
|
||||
}
|
||||
return (
|
||||
<StrRenderComponent
|
||||
<MemoizedStrRender
|
||||
{...baseInputProps}
|
||||
templateData={templateData}
|
||||
name={name}
|
||||
|
|
@ -101,10 +140,11 @@ export function ParameterRenderComponent({
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (templateData.type) {
|
||||
case "NestedDict":
|
||||
return (
|
||||
<DictComponent
|
||||
<MemoizedDict
|
||||
name={name ?? ""}
|
||||
{...baseInputProps}
|
||||
id={`dict_${id}`}
|
||||
|
|
@ -112,7 +152,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "dict":
|
||||
return (
|
||||
<KeypairListComponent
|
||||
<MemoizedKeypairList
|
||||
{...baseInputProps}
|
||||
isList={templateData.list ?? false}
|
||||
id={`keypair_${id}`}
|
||||
|
|
@ -120,7 +160,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "bool":
|
||||
return (
|
||||
<ToggleShadComponent
|
||||
<MemoizedToggleShad
|
||||
size="medium"
|
||||
{...baseInputProps}
|
||||
id={`toggle_${id}`}
|
||||
|
|
@ -128,7 +168,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "link":
|
||||
return (
|
||||
<LinkComponent
|
||||
<MemoizedLink
|
||||
{...baseInputProps}
|
||||
icon={templateData.icon}
|
||||
text={templateData.text}
|
||||
|
|
@ -137,7 +177,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "float":
|
||||
return (
|
||||
<FloatComponent
|
||||
<MemoizedFloat
|
||||
{...baseInputProps}
|
||||
id={`float_${id}`}
|
||||
rangeSpec={templateData.range_spec}
|
||||
|
|
@ -145,7 +185,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "int":
|
||||
return (
|
||||
<IntComponent
|
||||
<MemoizedInt
|
||||
{...baseInputProps}
|
||||
rangeSpec={templateData.range_spec}
|
||||
id={`int_${id}`}
|
||||
|
|
@ -153,7 +193,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "file":
|
||||
return (
|
||||
<InputFileComponent
|
||||
<MemoizedInputFile
|
||||
{...baseInputProps}
|
||||
fileTypes={templateData.fileTypes}
|
||||
id={`inputfile_${id}`}
|
||||
|
|
@ -161,7 +201,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "prompt":
|
||||
return (
|
||||
<PromptAreaComponent
|
||||
<MemoizedPromptArea
|
||||
{...baseInputProps}
|
||||
readonly={!!nodeClass.flow}
|
||||
field_name={name}
|
||||
|
|
@ -169,10 +209,10 @@ export function ParameterRenderComponent({
|
|||
/>
|
||||
);
|
||||
case "code":
|
||||
return <CodeAreaComponent {...baseInputProps} id={`codearea_${id}`} />;
|
||||
return <MemoizedCodeArea {...baseInputProps} id={`codearea_${id}`} />;
|
||||
case "table":
|
||||
return (
|
||||
<TableNodeComponent
|
||||
<MemoizedTableNode
|
||||
{...baseInputProps}
|
||||
description={templateData.info || "Add or edit data"}
|
||||
columns={templateData?.table_schema?.columns}
|
||||
|
|
@ -184,7 +224,7 @@ export function ParameterRenderComponent({
|
|||
);
|
||||
case "slider":
|
||||
return (
|
||||
<SliderComponent
|
||||
<MemoizedSlider
|
||||
{...baseInputProps}
|
||||
value={templateValue}
|
||||
rangeSpec={templateData.range_spec}
|
||||
|
|
@ -199,24 +239,21 @@ export function ParameterRenderComponent({
|
|||
/>
|
||||
);
|
||||
default:
|
||||
return <EmptyParameterComponent {...baseInputProps} />;
|
||||
return <MemoizedEmpty {...baseInputProps} />;
|
||||
}
|
||||
};
|
||||
}, [templateData, baseInputProps, name, id, nodeClass.flow]);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<RefreshParameterComponent
|
||||
templateData={templateData}
|
||||
disabled={disabled}
|
||||
nodeId={nodeId}
|
||||
editNode={editNode}
|
||||
nodeClass={nodeClass}
|
||||
handleNodeClass={handleNodeClass}
|
||||
name={name}
|
||||
>
|
||||
{renderComponent()}
|
||||
</RefreshParameterComponent>
|
||||
),
|
||||
[templateData, disabled, nodeId, editNode, nodeClass, name, templateValue],
|
||||
return (
|
||||
<RefreshParameterComponent
|
||||
templateData={templateData}
|
||||
disabled={disabled}
|
||||
nodeId={nodeId}
|
||||
editNode={editNode}
|
||||
nodeClass={nodeClass}
|
||||
handleNodeClass={handleNodeClass}
|
||||
name={name}
|
||||
>
|
||||
{useMemo(() => renderComponent(), [renderComponent])}
|
||||
</RefreshParameterComponent>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@ import {
|
|||
Variants,
|
||||
} from "framer-motion";
|
||||
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";
|
||||
|
||||
type DisclosureContextType = {
|
||||
|
|
@ -28,38 +36,33 @@ type DisclosureProviderProps = {
|
|||
variants?: { expanded: Variant; collapsed: Variant };
|
||||
};
|
||||
|
||||
function DisclosureProvider({
|
||||
const DisclosureProvider = memo(function DisclosureProvider({
|
||||
children,
|
||||
open: openProp,
|
||||
onOpenChange,
|
||||
variants,
|
||||
}: DisclosureProviderProps) {
|
||||
const [internalOpenValue, setInternalOpenValue] = useState<boolean>(openProp);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalOpenValue(openProp);
|
||||
}, [openProp]);
|
||||
|
||||
const toggle = () => {
|
||||
const newOpen = !internalOpenValue;
|
||||
setInternalOpenValue(newOpen);
|
||||
const toggle = useCallback(() => {
|
||||
if (onOpenChange) {
|
||||
onOpenChange(newOpen);
|
||||
onOpenChange(!openProp);
|
||||
}
|
||||
};
|
||||
}, [onOpenChange, openProp]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
open: openProp,
|
||||
toggle,
|
||||
variants,
|
||||
}),
|
||||
[openProp, toggle, variants],
|
||||
);
|
||||
|
||||
return (
|
||||
<DisclosureContext.Provider
|
||||
value={{
|
||||
open: internalOpenValue,
|
||||
toggle,
|
||||
variants,
|
||||
}}
|
||||
>
|
||||
<DisclosureContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</DisclosureContext.Provider>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function useDisclosure() {
|
||||
const context = useContext(DisclosureContext);
|
||||
|
|
@ -78,7 +81,7 @@ type DisclosureProps = {
|
|||
transition?: Transition;
|
||||
};
|
||||
|
||||
export function Disclosure({
|
||||
export const Disclosure = memo(function Disclosure({
|
||||
open: openProp = false,
|
||||
onOpenChange,
|
||||
children,
|
||||
|
|
@ -86,6 +89,8 @@ export function Disclosure({
|
|||
transition,
|
||||
variants,
|
||||
}: DisclosureProps) {
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
|
||||
return (
|
||||
<MotionConfig transition={transition}>
|
||||
<div className={className}>
|
||||
|
|
@ -94,15 +99,15 @@ export function Disclosure({
|
|||
onOpenChange={onOpenChange}
|
||||
variants={variants}
|
||||
>
|
||||
{React.Children.toArray(children)[0]}
|
||||
{React.Children.toArray(children)[1]}
|
||||
{childrenArray[0]}
|
||||
{childrenArray[1]}
|
||||
</DisclosureProvider>
|
||||
</div>
|
||||
</MotionConfig>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export function DisclosureTrigger({
|
||||
const DisclosureTrigger = memo(function DisclosureTrigger({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
|
|
@ -111,34 +116,54 @@ export function DisclosureTrigger({
|
|||
}) {
|
||||
const { toggle, open } = useDisclosure();
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: { key: string; preventDefault: () => void }) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
},
|
||||
[toggle],
|
||||
);
|
||||
|
||||
const childProps = useMemo(
|
||||
() => ({
|
||||
onClick: toggle,
|
||||
role: "button",
|
||||
"aria-expanded": open,
|
||||
tabIndex: 0,
|
||||
onKeyDown: handleKeyDown,
|
||||
}),
|
||||
[toggle, open, handleKeyDown],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{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 === " ") {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
},
|
||||
className: cn(
|
||||
className,
|
||||
(child as React.ReactElement).props.className,
|
||||
),
|
||||
...(child as React.ReactElement).props,
|
||||
})
|
||||
: 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,
|
||||
className,
|
||||
}: {
|
||||
|
|
@ -148,21 +173,13 @@ export function DisclosureContent({
|
|||
const { open, variants } = useDisclosure();
|
||||
const uniqueId = useId();
|
||||
|
||||
const BASE_VARIANTS: Variants = {
|
||||
expanded: {
|
||||
height: "auto",
|
||||
opacity: 1,
|
||||
},
|
||||
collapsed: {
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const combinedVariants = {
|
||||
expanded: { ...BASE_VARIANTS.expanded, ...variants?.expanded },
|
||||
collapsed: { ...BASE_VARIANTS.collapsed, ...variants?.collapsed },
|
||||
};
|
||||
const combinedVariants = useMemo(
|
||||
() => ({
|
||||
expanded: { ...BASE_VARIANTS.expanded, ...variants?.expanded },
|
||||
collapsed: { ...BASE_VARIANTS.collapsed, ...variants?.collapsed },
|
||||
}),
|
||||
[variants],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("overflow-hidden", className)}>
|
||||
|
|
@ -181,7 +198,9 @@ export function DisclosureContent({
|
|||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export { DisclosureContent, DisclosureTrigger };
|
||||
|
||||
export default {
|
||||
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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook"; // Import useHotkeys
|
||||
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureContent,
|
||||
DisclosureTrigger,
|
||||
} from "@/components/ui/disclosure";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
|
@ -18,12 +15,9 @@ import {
|
|||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useAddComponent } from "@/hooks/useAddComponent";
|
||||
|
|
@ -39,12 +33,11 @@ import useAlertStore from "../../../../stores/alertStore";
|
|||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import { useTypesStore } from "../../../../stores/typesStore";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { SidebarFilterComponent } from "../extraSidebarComponent/sidebarFilterComponent";
|
||||
import sensitiveSort from "../extraSidebarComponent/utils/sensitive-sort";
|
||||
import ShortcutDisplay from "../nodeToolbarComponent/shortcutDisplay";
|
||||
import { CategoryGroup } from "./components/categoryGroup";
|
||||
import NoResultsMessage from "./components/emptySearchComponent";
|
||||
import FeatureToggles from "./components/featureTogglesComponent";
|
||||
import SidebarMenuButtons from "./components/sidebarFooterButtons";
|
||||
import { SidebarHeaderComponent } from "./components/sidebarHeader";
|
||||
import SidebarItemsList from "./components/sidebarItemsList";
|
||||
import { applyBetaFilter } from "./helpers/apply-beta-filter";
|
||||
import { applyEdgeFilter } from "./helpers/apply-edge-filter";
|
||||
|
|
@ -58,16 +51,104 @@ const CATEGORIES = SIDEBAR_CATEGORIES;
|
|||
const BUNDLES = SIDEBAR_BUNDLES;
|
||||
|
||||
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 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 [dataFilter, setFilterData] = useState(data);
|
||||
const [search, setSearch] = useState("");
|
||||
|
|
@ -231,10 +312,14 @@ export function FlowSidebarComponent() {
|
|||
}
|
||||
};
|
||||
|
||||
function handleSearchInput(e: string) {
|
||||
setSearch(e);
|
||||
filterComponents();
|
||||
}
|
||||
const handleSearchInput = useCallback(
|
||||
(value: string) => {
|
||||
setSearch(value);
|
||||
const filtered = getFilteredData(value, data, fuse);
|
||||
setFilterData(filtered);
|
||||
},
|
||||
[data, fuse],
|
||||
);
|
||||
|
||||
function onDragStart(
|
||||
event: React.DragEvent<any>,
|
||||
|
|
@ -252,24 +337,6 @@ export function FlowSidebarComponent() {
|
|||
event.dataTransfer.setData("genericNode", JSON.stringify(data));
|
||||
}
|
||||
|
||||
const customComponent = useMemo(() => {
|
||||
return data?.["custom_component"]?.["CustomComponent"] ?? null;
|
||||
}, [data]);
|
||||
|
||||
const handleKeyDown = (
|
||||
e: React.KeyboardEvent<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(
|
||||
(item) =>
|
||||
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
|
||||
|
|
@ -286,9 +353,6 @@ export function FlowSidebarComponent() {
|
|||
setOpenCategories([]);
|
||||
}
|
||||
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const chatInputAdded = checkChatInput(nodes);
|
||||
|
||||
const handleInputFocus = useCallback(
|
||||
(event: React.FocusEvent<HTMLInputElement>) => {
|
||||
setIsInputFocused(true);
|
||||
|
|
@ -316,156 +380,40 @@ export function FlowSidebarComponent() {
|
|||
data-testid="shad-sidebar"
|
||||
className="noflow"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<SidebarHeaderComponent
|
||||
showConfig={showConfig}
|
||||
setShowConfig={setShowConfig}
|
||||
showBeta={showBeta}
|
||||
setShowBeta={setShowBeta}
|
||||
showLegacy={showLegacy}
|
||||
setShowLegacy={setShowLegacy}
|
||||
searchInputRef={searchInputRef}
|
||||
isInputFocused={isInputFocused}
|
||||
search={search}
|
||||
handleInputFocus={handleInputFocus}
|
||||
handleInputBlur={handleInputBlur}
|
||||
handleInputChange={handleInputChange}
|
||||
filterType={filterType}
|
||||
setFilterEdge={setFilterEdge}
|
||||
setFilterData={setFilterData}
|
||||
data={data}
|
||||
/>
|
||||
<SidebarContent>
|
||||
{hasResults ? (
|
||||
<>
|
||||
{hasCategoryItems && (
|
||||
<SidebarGroup className="p-3">
|
||||
<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}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
/>
|
||||
</DisclosureContent>
|
||||
</SidebarMenuItem>
|
||||
</Disclosure>
|
||||
),
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<CategoryGroup
|
||||
dataFilter={dataFilter}
|
||||
sortedCategories={sortedCategories}
|
||||
CATEGORIES={CATEGORIES}
|
||||
openCategories={openCategories}
|
||||
setOpenCategories={setOpenCategories}
|
||||
search={search}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
/>
|
||||
)}
|
||||
{hasBundleItems && (
|
||||
<SidebarGroup className="p-3">
|
||||
|
|
@ -501,7 +449,7 @@ export function FlowSidebarComponent() {
|
|||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) =>
|
||||
handleKeyDown(e, item.name)
|
||||
handleKeyDownInput(e, item.name)
|
||||
}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
data-testid={`disclosure-bundles-${item.display_name.toLocaleLowerCase()}`}
|
||||
|
|
@ -553,3 +501,7 @@ export function FlowSidebarComponent() {
|
|||
</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;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -226,6 +226,8 @@ test(
|
|||
.getByPlaceholder("Empty")
|
||||
.textContent();
|
||||
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
await page.getByTestId("btn-close-modal").click();
|
||||
|
||||
await page.getByTestId("textarea_str_input_value").first().fill(",");
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ test(
|
|||
|
||||
await page.getByTestId("fit_view").click();
|
||||
|
||||
expect(await page.getByText("Saved").isVisible()).toBeTruthy();
|
||||
expect(await page.getByText("Saved").last().isVisible()).toBeTruthy();
|
||||
|
||||
await page
|
||||
.getByText("Saved")
|
||||
|
|
@ -71,11 +71,19 @@ test(
|
|||
|
||||
await page.getByTestId("icon-ChevronLeft").last().click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Unsaved changes will be permanently lost."),
|
||||
).toBeVisible();
|
||||
try {
|
||||
await page.waitForSelector(
|
||||
'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();
|
||||
|
||||
|
|
@ -83,21 +91,34 @@ test(
|
|||
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").fill("NVIDIA");
|
||||
|
||||
await page.waitForSelector('[data-testid="modelsNVIDIA"]', {
|
||||
timeout: 3000,
|
||||
});
|
||||
await page.keyboard.press("Escape");
|
||||
await page.locator('//*[@id="react-flow-id"]').click();
|
||||
|
||||
await page
|
||||
.getByTestId("modelsNVIDIA")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
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.getByTestId("add-component-button-nvidia").click();
|
||||
} catch (error) {
|
||||
console.error("Failed to hover or find add component button:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wait for fit view button
|
||||
await page.waitForSelector('[data-testid="fit_view"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue