refactor: add components for node name, description, and status (#3248)

* remove repeated code

* refactor: remove useless code

* feat: sort input parameters in GenericNode renderInputParameter

* refactor: remove unused code in GenericNode component

* refactor: add NodeName component for displaying and editing node names

* refactor: add NodeDescription component for displaying and editing node descriptions

* fix import and add autofocus on nodeName

* feat: add NodeStatus component for displaying and managing node status

* [autofix.ci] apply automated fixes

* refactor: remove unused code in GenericNode component

* fix bugs on minimize

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
anovazzi1 2024-08-13 14:37:33 -03:00 committed by GitHub
commit 64aecddeca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 457 additions and 463 deletions

View file

@ -0,0 +1,100 @@
import { Textarea } from "@/components/ui/textarea";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { handleKeyDown } from "@/utils/reactflowUtils";
import { cn } from "@/utils/utils";
import { useEffect, useState } from "react";
import Markdown from "react-markdown";
export default function NodeDescription({
description,
selected,
nodeId,
}: {
description?: string;
selected: boolean;
nodeId: string;
}) {
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(description);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const setNode = useFlowStore((state) => state.setNode);
useEffect(() => {
if (!selected) {
setInputDescription(false);
}
}, [selected]);
useEffect(() => {
setNodeDescription(description);
}, [description]);
return (
<div className="generic-node-desc">
{inputDescription ? (
<Textarea
className="nowheel min-h-40"
autoFocus
onBlur={() => {
setInputDescription(false);
setNodeDescription(nodeDescription);
setNode(nodeId, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
description: nodeDescription,
},
},
}));
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
setNodeDescription(nodeDescription);
setNode(nodeId, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
description: nodeDescription,
},
},
}));
}
}}
/>
) : (
<div
className={cn(
"nodoubleclick generic-node-desc-text cursor-text word-break-break-word",
description === "" || !description ? "font-light italic" : "",
)}
onDoubleClick={(e) => {
setInputDescription(true);
takeSnapshot();
}}
>
{description === "" || !description ? (
"Double Click to Edit Description"
) : (
<Markdown className="markdown prose flex flex-col text-primary word-break-break-word dark:prose-invert">
{String(description)}
</Markdown>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,78 @@
import InputComponent from "@/components/inputComponent";
import ShadTooltip from "@/components/shadTooltipComponent";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { useEffect, useState } from "react";
export default function NodeName({
display_name,
selected,
nodeId,
}: {
display_name?: string;
selected: boolean;
nodeId: string;
}) {
const [inputName, setInputName] = useState(false);
const [nodeName, setNodeName] = useState(display_name);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const setNode = useFlowStore((state) => state.setNode);
useEffect(() => {
if (!selected) {
setInputName(false);
}
}, [selected]);
useEffect(() => {
setNodeName(display_name);
}, [display_name]);
return inputName ? (
<div>
<InputComponent
onBlur={() => {
setInputName(false);
if (nodeName?.trim() !== "") {
setNodeName(nodeName);
setNode(nodeId, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
display_name: nodeName,
},
},
}));
} else {
setNodeName(display_name);
}
}}
value={nodeName}
autoFocus
onChange={setNodeName}
password={false}
blurOnEnter={true}
id={`input-title-${display_name}`}
/>
</div>
) : (
<div className="group flex items-center gap-1">
<ShadTooltip content={display_name}>
<div
onDoubleClick={(event) => {
setInputName(true);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
data-testid={"title-" + display_name}
className="nodoubleclick generic-node-tooltip-div cursor-text text-primary"
>
{display_name}
</div>
</ShadTooltip>
</div>
);
}

View file

@ -0,0 +1,190 @@
import { getSpecificClassFromBuildStatus } from "@/CustomNodes/helpers/get-class-from-build-status";
import useIconStatus from "@/CustomNodes/hooks/use-icons-status";
import useUpdateValidationStatus from "@/CustomNodes/hooks/use-update-validation-status";
import useValidationStatusString from "@/CustomNodes/hooks/use-validation-status-string";
import ShadTooltip from "@/components/shadTooltipComponent";
import { Button } from "@/components/ui/button";
import {
RUN_TIMESTAMP_PREFIX,
STATUS_BUILD,
STATUS_BUILDING,
STATUS_INACTIVE,
} from "@/constants/constants";
import { BuildStatus } from "@/constants/enums";
import { useDarkStore } from "@/stores/darkStore";
import useFlowStore from "@/stores/flowStore";
import { useShortcutsStore } from "@/stores/shortcuts";
import { VertexBuildTypeAPI } from "@/types/api";
import { classNames } from "@/utils/utils";
import { useEffect, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import IconComponent from "../../../../components/genericIconComponent";
export default function NodeStatus({
nodeId,
display_name,
selected,
setBorderColor,
frozen,
showNode,
}: {
nodeId: string;
display_name: string;
selected: boolean;
setBorderColor: (color: string) => void;
frozen?: boolean;
showNode: boolean;
}) {
const [validationString, setValidationString] = useState<string>("");
const [validationStatus, setValidationStatus] =
useState<VertexBuildTypeAPI | null>(null);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[nodeId]?.status,
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[nodeId]?.timestamp,
);
const iconStatus = useIconStatus(buildStatus, validationStatus);
const buildFlow = useFlowStore((state) => state.buildFlow);
const isBuilding = useFlowStore((state) => state.isBuilding);
const setNode = useFlowStore((state) => state.setNode);
const version = useDarkStore((state) => state.version);
const isDark = useDarkStore((state) => state.dark);
function handlePlayWShortcut() {
if (buildStatus === BuildStatus.BUILDING || isBuilding || !selected) return;
setValidationStatus(null);
buildFlow({ stopNodeId: nodeId });
}
const play = useShortcutsStore((state) => state.play);
const flowPool = useFlowStore((state) => state.flowPool);
useHotkeys(play, handlePlayWShortcut, { preventDefault: true });
useValidationStatusString(validationStatus, setValidationString);
useUpdateValidationStatus(nodeId, flowPool, setValidationStatus);
const getBaseBorderClass = (selected) => {
let className = selected
? "border border-ring hover:shadow-node"
: "border hover:shadow-node";
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
return frozen ? frozenClass : className;
};
const getNodeSizeClass = (showNode) =>
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
const getNodeBorderClassName = (
selected: boolean,
showNode: boolean,
buildStatus: BuildStatus | undefined,
validationStatus: VertexBuildTypeAPI | null,
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus,
isDark,
);
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
const names = classNames(
baseBorderClass,
nodeSizeClass,
"generic-node-div group/node",
specificClassFromBuildStatus,
);
return names;
};
useEffect(() => {
console.log(selected);
setBorderColor(
getNodeBorderClassName(selected, showNode, buildStatus, validationStatus),
);
}, [selected, showNode, buildStatus, validationStatus, frozen]);
useEffect(() => {
if (buildStatus === BuildStatus.BUILT && !isBuilding) {
setNode(
nodeId,
(old) => {
return {
...old,
data: {
...old.data,
node: {
...old.data.node,
lf_version: version,
},
},
};
},
false,
);
}
}, [buildStatus, isBuilding]);
return (
<>
<div className="flex flex-shrink-0 items-center gap-2">
<ShadTooltip
content={
buildStatus === BuildStatus.BUILDING ? (
<span> {STATUS_BUILDING} </span>
) : buildStatus === BuildStatus.INACTIVE ? (
<span> {STATUS_INACTIVE} </span>
) : !validationStatus ? (
<span className="flex">{STATUS_BUILD}</span>
) : (
<div className="max-h-100 p-2">
<div className="max-h-80 overflow-auto">
{validationString && (
<div className="ml-1 pb-2 text-status-red">
{validationString}
</div>
)}
{lastRunTime && (
<div className="justify-left flex font-normal text-muted-foreground">
<div>{RUN_TIMESTAMP_PREFIX}</div>
<div className="ml-1 text-status-blue">{lastRunTime}</div>
</div>
)}
</div>
<div className="justify-left flex font-normal text-muted-foreground">
<div>Duration:</div>
<div className="ml-1 text-status-blue">
{validationStatus?.data.duration}
</div>
</div>
</div>
)
}
side="bottom"
>
<div className="cursor-help">{iconStatus}</div>
</ShadTooltip>
{showNode && (
<Button
onClick={() => {
if (buildStatus === BuildStatus.BUILDING || isBuilding) return;
setValidationStatus(null);
buildFlow({ stopNodeId: nodeId });
}}
unstyled
className="group p-1"
>
<div data-testid={`button_run_` + display_name.toLowerCase()}>
<IconComponent
name="Play"
className={
"h-5 w-5 fill-current stroke-2 text-muted-foreground transition-all group-hover:text-medium-indigo group-hover/node:opacity-100"
}
/>
</div>
</Button>
)}
</div>
</>
);
}

View file

@ -1,54 +1,41 @@
import emojiRegex from "emoji-regex";
import { useEffect, useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import Markdown from "react-markdown";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import IconComponent, {
ForwardedIconComponent,
} from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import ShadTooltip from "../../components/shadTooltipComponent";
import { Button } from "../../components/ui/button";
import { Textarea } from "../../components/ui/textarea";
import {
RUN_TIMESTAMP_PREFIX,
STATUS_BUILD,
STATUS_BUILDING,
STATUS_INACTIVE,
TOOLTIP_OUTDATED_NODE,
} from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import { TOOLTIP_OUTDATED_NODE } from "../../constants/constants";
import { postCustomComponent } from "../../controllers/API";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
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 { OutputFieldType } from "../../types/api";
import { NodeDataType } from "../../types/flow";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, cn } from "../../utils/utils";
import { countHandlesFn } from "../helpers/count-handles";
import { getSpecificClassFromBuildStatus } from "../helpers/get-class-from-build-status";
import { getNodeInputColors } from "../helpers/get-node-input-colors";
import { getNodeOutputColors } from "../helpers/get-node-output-colors";
import useCheckCodeValidity from "../hooks/use-check-code-validity";
import useIconNodeRender from "../hooks/use-icon-render";
import useIconStatus from "../hooks/use-icons-status";
import useUpdateNodeCode from "../hooks/use-update-node-code";
import useUpdateValidationStatus from "../hooks/use-update-validation-status";
import useValidationStatusString from "../hooks/use-validation-status-string";
import getFieldTitle from "../utils/get-field-title";
import sortFields from "../utils/sort-fields";
import NodeDescription from "./components/NodeDescription";
import NodeInputField from "./components/NodeInputField";
import NodeName from "./components/NodeName";
import NodeOutputField from "./components/NodeOutputfield";
import NodeStatus from "./components/NodeStatus";
export default function GenericNode({
data,
selected,
}: {
data: NodeDataType;
@ -56,42 +43,18 @@ export default function GenericNode({
xPos?: number;
yPos?: number;
}): JSX.Element {
const preventDefault = true;
const types = useTypesStore((state) => state.types);
const templates = useTypesStore((state) => state.templates);
const deleteNode = useFlowStore((state) => state.deleteNode);
const flowPool = useFlowStore((state) => state.flowPool);
const buildFlow = useFlowStore((state) => state.buildFlow);
const setNode = useFlowStore((state) => state.setNode);
const updateNodeInternals = useUpdateNodeInternals();
const setErrorData = useAlertStore((state) => state.setErrorData);
const isDark = useDarkStore((state) => state.dark);
const version = useDarkStore((state) => state.version);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [inputName, setInputName] = useState(false);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!,
);
const [isOutdated, setIsOutdated] = useState(false);
const [isUserEdited, setIsUserEdited] = useState(false);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status,
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp,
);
const [validationStatus, setValidationStatus] =
useState<VertexBuildTypeAPI | null>(null);
const [handles, setHandles] = useState<number>(0);
const [validationString, setValidationString] = useState<string>("");
const iconStatus = useIconStatus(buildStatus, validationStatus);
const [borderColor, setBorderColor] = useState<string>("");
const [showNode, setShowNode] = useState(data.showNode ?? true);
// State for outline color
const isBuilding = useFlowStore((state) => state.isBuilding);
const updateNodeCode = useUpdateNodeCode(
data?.id,
@ -118,47 +81,6 @@ export default function GenericNode({
);
};
const renderIconStatus = () => {
return <div className="cursor-help">{iconStatus}</div>;
};
const getNodeBorderClassName = (
selected: boolean,
showNode: boolean,
buildStatus: BuildStatus | undefined,
validationStatus: VertexBuildTypeAPI | null,
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus,
isDark,
);
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
const names = classNames(
baseBorderClass,
nodeSizeClass,
"generic-node-div group/node",
specificClassFromBuildStatus,
);
return names;
};
// const [openWDoubleCLick, setOpenWDoubleCLick] = useState(false);
const getBaseBorderClass = (selected) => {
let className = selected
? "border border-ring hover:shadow-node"
: "border hover:shadow-node";
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
return data.node?.frozen ? frozenClass : className;
};
const getNodeSizeClass = (showNode) =>
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
const nameEditable = true;
const isEmoji = emojiRegex().test(data?.node?.icon!);
if (!data.node!.template) {
@ -174,8 +96,6 @@ export default function GenericNode({
}
useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
useUpdateValidationStatus(data?.id, flowPool, setValidationStatus);
useValidationStatusString(validationStatus, setValidationString);
const iconNodeRender = useIconNodeRender(
data,
@ -197,46 +117,10 @@ export default function GenericNode({
countHandles();
}, [data, data.node]);
useEffect(() => {
if (!selected) {
setInputName(false);
setInputDescription(false);
}
}, [selected]);
useEffect(() => {
setNodeDescription(data.node!.description);
}, [data.node!.description]);
useEffect(() => {
setNodeName(data.node!.display_name);
}, [data.node!.display_name]);
useEffect(() => {
setShowNode(data.showNode ?? true);
}, [data.showNode]);
useEffect(() => {
if (buildStatus === BuildStatus.BUILT && !isBuilding) {
setNode(
data.id,
(old) => {
return {
...old,
data: {
...old.data,
node: {
...old.data.node,
lf_version: version,
},
},
};
},
false,
);
}
}, [buildStatus, isBuilding]);
const [loadingUpdate, setLoadingUpdate] = useState(false);
const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
@ -286,17 +170,8 @@ export default function GenericNode({
const hiddenOutputs =
data.node!.outputs?.filter((output) => output.hidden) ?? [];
function handlePlayWShortcut() {
if (buildStatus === BuildStatus.BUILDING || isBuilding || !selected) return;
setValidationStatus(null);
buildFlow({ stopNodeId: data.id });
}
const update = useShortcutsStore((state) => state.update);
const play = useShortcutsStore((state) => state.play);
useHotkeys(update, handleUpdateCodeWShortcut, { preventDefault });
useHotkeys(play, handlePlayWShortcut, { preventDefault });
useHotkeys(update, handleUpdateCodeWShortcut, { preventDefault: true });
const shortcuts = useShortcutsStore((state) => state.shortcuts);
@ -376,24 +251,55 @@ export default function GenericNode({
isUserEdited,
selected,
shortcuts,
// openWDoubleCLick,
// setOpenWDoubleCLick,
]);
const renderInputParameter = Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) => sortFields(a, b, data.node?.field_order ?? []))
.map(
(templateField: string, idx) =>
data.node!.template[templateField]?.show &&
!data.node!.template[templateField]?.advanced && (
<NodeInputField
key={scapedJSONStringfy({
inputTypes: data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
colors={getNodeInputColors(
data.node?.template[templateField].input_types,
data.node?.template[templateField].type,
types,
)}
title={getFieldTitle(data.node?.template!, templateField)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join("\n") ??
data.node?.template[templateField].type
}
required={data.node!.template[templateField].required}
id={{
inputTypes: data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
type={data.node?.template[templateField].type}
optionalHandle={data.node?.template[templateField].input_types}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
),
);
return (
<>
{memoizedNodeToolbarComponent}
<div
// onDoubleClick={(event) => {
// if (!isWrappedWithClass(event, "nodoubleclick"))
// setOpenWDoubleCLick(true);
// }}
className={getNodeBorderClassName(
selected,
showNode,
buildStatus,
validationStatus,
)}
>
<div className={borderColor}>
{data.node?.beta && showNode && (
<div className="beta-badge-wrapper">
<div className="beta-badge-content">BETA</div>
@ -419,68 +325,25 @@ export default function GenericNode({
{iconNodeRender()}
{showNode && (
<div className="generic-node-tooltip-div">
{nameEditable && inputName ? (
<div>
<InputComponent
onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
setNodeName(nodeName);
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
display_name: nodeName,
},
},
}));
} else {
setNodeName(data.node!.display_name);
}
}}
value={nodeName}
onChange={setNodeName}
password={false}
blurOnEnter={true}
id={`input-title-${data.node?.display_name}`}
/>
</div>
) : (
<div className="group flex items-center gap-1">
<ShadTooltip content={data.node?.display_name}>
<div
onDoubleClick={(event) => {
if (nameEditable) {
setInputName(true);
}
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
data-testid={"title-" + data.node?.display_name}
className="nodoubleclick generic-node-tooltip-div cursor-text text-primary"
>
{data.node?.display_name}
</div>
</ShadTooltip>
{isOutdated && !isUserEdited && (
<ShadTooltip content={TOOLTIP_OUTDATED_NODE}>
<Button
onClick={handleUpdateCode}
unstyled
className={"group p-1"}
loading={loadingUpdate}
>
<IconComponent
name="AlertTriangle"
className="h-5 w-5 fill-status-yellow text-muted"
/>
</Button>
</ShadTooltip>
)}
</div>
<NodeName
display_name={data.node?.display_name}
nodeId={data.id}
selected={selected}
/>
{isOutdated && !isUserEdited && (
<ShadTooltip content={TOOLTIP_OUTDATED_NODE}>
<Button
onClick={handleUpdateCode}
unstyled
className={"group p-1"}
loading={loadingUpdate}
>
<IconComponent
name="AlertTriangle"
className="h-5 w-5 fill-status-yellow text-muted"
/>
</Button>
</ShadTooltip>
)}
</div>
)}
@ -488,271 +351,34 @@ export default function GenericNode({
<div>
{!showNode && (
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map(
(templateField: string, idx) =>
data.node!.template[templateField]?.show &&
!data.node!.template[templateField]?.advanced && (
<NodeInputField
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
colors={getNodeInputColors(
data.node?.template[templateField].input_types,
data.node?.template[templateField].type,
types,
)}
title={getFieldTitle(
data.node?.template!,
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[
templateField
].input_types?.join("\n") ??
data.node?.template[templateField].type
}
required={
data.node!.template[templateField].required
}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
),
)}
{renderInputParameter}
{shownOutputs &&
shownOutputs.length > 0 &&
renderOutputParameter(shownOutputs[0], 0)}
</>
)}
</div>
{showNode && (
<>
<div className="flex flex-shrink-0 items-center gap-2">
<ShadTooltip
content={
buildStatus === BuildStatus.BUILDING ? (
<span> {STATUS_BUILDING} </span>
) : buildStatus === BuildStatus.INACTIVE ? (
<span> {STATUS_INACTIVE} </span>
) : !validationStatus ? (
<span className="flex">{STATUS_BUILD}</span>
) : (
<div className="max-h-100 p-2">
<div className="max-h-80 overflow-auto">
{validationString && (
<div className="ml-1 pb-2 text-status-red">
{validationString}
</div>
)}
{lastRunTime && (
<div className="justify-left flex font-normal text-muted-foreground">
<div>{RUN_TIMESTAMP_PREFIX}</div>
<div className="ml-1 text-status-blue">
{lastRunTime}
</div>
</div>
)}
</div>
<div className="justify-left flex font-normal text-muted-foreground">
<div>Duration:</div>
<div className="ml-1 text-status-blue">
{validationStatus?.data.duration}
</div>
</div>
</div>
)
}
side="bottom"
>
{renderIconStatus()}
</ShadTooltip>
<Button
onClick={() => {
if (buildStatus === BuildStatus.BUILDING || isBuilding)
return;
setValidationStatus(null);
buildFlow({ stopNodeId: data.id });
}}
unstyled
className="group p-1"
>
<div
data-testid={
`button_run_` + data?.node?.display_name.toLowerCase()
}
>
<IconComponent
name="Play"
className={
"h-5 w-5 fill-current stroke-2 text-muted-foreground transition-all group-hover:text-medium-indigo group-hover/node:opacity-100"
}
/>
</div>
</Button>
</div>
</>
)}
<NodeStatus
frozen={data.node?.frozen}
showNode={showNode}
display_name={data.node?.display_name!}
nodeId={data.id}
selected={selected}
setBorderColor={setBorderColor}
/>
</div>
</div>
{showNode && (
<div
className={cn(
showNode
? data.node?.description === "" && !nameEditable
? "pb-8"
: "pb-8 pt-5"
: "",
"relative",
)}
>
<div className="relative pb-8 pt-5">
{/* increase height!! */}
<div className="generic-node-desc">
{showNode && nameEditable && inputDescription ? (
<Textarea
className="nowheel min-h-40"
autoFocus
onBlur={() => {
setInputDescription(false);
setInputName(false);
setNodeDescription(nodeDescription);
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
description: nodeDescription,
},
},
}));
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
setNodeDescription(nodeDescription);
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
description: nodeDescription,
},
},
}));
}
}}
/>
) : (
<div
className={cn(
"nodoubleclick generic-node-desc-text cursor-text word-break-break-word",
(data.node?.description === "" ||
!data.node?.description) &&
nameEditable
? "font-light italic"
: "",
)}
onDoubleClick={(e) => {
setInputDescription(true);
takeSnapshot();
}}
>
{(data.node?.description === "" || !data.node?.description) &&
nameEditable ? (
"Double Click to Edit Description"
) : (
<Markdown className="markdown prose flex flex-col text-primary word-break-break-word dark:prose-invert">
{String(data.node?.description)}
</Markdown>
)}
</div>
)}
</div>
<NodeDescription
description={data.node?.description}
nodeId={data.id}
selected={selected}
/>
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) => sortFields(a, b, data.node?.field_order ?? []))
.map((templateField: string, idx) => (
<div key={idx}>
{data.node!.template[templateField]?.show &&
!data.node!.template[templateField]?.advanced ? (
<NodeInputField
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
colors={getNodeInputColors(
data.node?.template[templateField].input_types,
data.node?.template[templateField].type,
types,
)}
title={getFieldTitle(
data.node?.template!,
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join(
"\n",
) ?? data.node?.template[templateField].type
}
required={data.node!.template[templateField].required}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
) : (
<></>
)}
</div>
))}
{renderInputParameter}
<div
className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "",

View file

@ -636,7 +636,7 @@ export function handleKeyDown(
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLTextAreaElement>,
inputValue: string | string[] | null,
inputValue: string | string[] | null | undefined,
block: string,
) {
//condition to fix bug control+backspace on Windows/Linux