feat: Update node name and description UX editing (#5920)
* ✨ (NodeName/index.tsx): add cursor-grab class to improve user experience when dragging the node ✨ (NodeName/index.tsx): add nodrag class to prevent text selection when dragging the node ✨ (NodeStatus/index.tsx): add nodrag class to prevent text selection when dragging the node ✨ (GenericNode/index.tsx): add nopan, nodelete, nodrag, noflow classes to improve node dragging behavior * 📝 (NodeDescription/index.tsx): Update cursor style to 'auto' for better user experience 📝 (NodeName/index.tsx): Update cursor style to 'auto' for better user experience 📝 (NodeOutputfield/index.tsx): Add cursor style 'pointer' to improve interactivity * ✨ (NodeDescription/index.tsx): Add support for editing node description when selected and editNameDescription is true ✨ (NodeName/index.tsx): Add support for editing node name when selected and editNameDescription is true 🔧 (GenericNode/index.tsx): Introduce useAlternate hook to handle toggling editNameDescription state 📝 (use-alternate.tsx): Add custom hook useAlternate to handle toggling boolean state 🔧 (style/index.css): Add new CSS variable --zinc-foreground for styling purposes 🔧 (tailwind.config.mjs): Add new tailwind color variable "zinc-foreground" for styling purposes * 📝 (NodeDescription/index.tsx): Remove unnecessary setInputDescription call and update useEffect dependencies for better performance 📝 (NodeDescription/index.tsx): Update className for Textarea component to improve styling and readability 📝 (NodeDescription/index.tsx): Update className for generic-node-desc-text to improve styling and cursor behavior 📝 (NodeName/index.tsx): Remove unnecessary setInputName call and update useEffect dependencies for better performance 📝 (NodeName/index.tsx): Update className for span element to improve cursor behavior and styling 📝 (GenericNode/index.tsx): Add useRef for node element and implement useChangeOnUnfocus hook for better handling of focus events 📝 (GenericNode/index.tsx): Update className for pencil icon based on editNameDescription state for better visual feedback 📝 (GenericNode/index.tsx): Add editNameDescription to dependencies of useCallback to prevent unnecessary re-renders 📝 (GenericNode/index.tsx): Add editNameDescription to dependencies of useEffect to handle changes in editNameDescription state 📝 (use-change-on-unfocus.tsx): Implement custom hook useChangeOnUnfocus for handling focus events and state changes * Refactor NodeDescription to remove old logic and variables * Refactor NodeName component to remove unnecessary logic and variables * [autofix.ci] apply automated fixes * ✨ (NodeDescription/index.tsx): Add functionality to edit node description and handle events like blur, key down, and double click for sticky notes 📝 (NoteNode/index.tsx): Introduce useAlternate hook to toggle edit mode for node description in NoteNode component * ♻️ (NoteNode/index.tsx): refactor useAlternate hook usage to simplify code and improve readability * 🔧 (GenericNode/index.tsx): refactor className to conditionally apply translate-x styles based on showNode state for improved UI responsiveness * 📝 (NodeDescription/index.tsx): Refactor handleBlurFn and handleKeyDownFn to improve code readability and maintainability 📝 (NodeName/index.tsx): Refactor handleBlur and handleKeyDown functions for better code organization and readability 📝 (GenericNode/index.tsx): Update toggleEditNameDescription prop to setEditNameDescription for consistency and clarity 📝 (use-change-on-unfocus.tsx): Remove unnecessary handleEscape function and handleBlur event listener for better code simplicity and performance * 📝 (NodeDescription/index.tsx): Update CSS class name to use 'focus-border-primary' instead of 'focus-border-black' for consistency and clarity 📝 (GenericNode/index.tsx): Add data-testid attribute to save and edit name description buttons for testing purposes 📝 (edit-name-description-node.spec.ts): Add test to verify user can edit name and description of a node in the UI * ✨ (GenericNode/index.tsx): Add functionality to show and hide toolbar with animation based on node selection status 📝 (get-class-toolbar-transform.ts): Create helper function to determine transform classes for toolbar animation based on showToolbar and showNode status * ✨ (NodeDescription/index.tsx): add setHasChangedNodeDescription prop to update parent component when node description changes ✨ (NodeName/index.tsx): add setHasChangedNodeDescription prop to update parent component when node name changes ✨ (GenericNode/index.tsx): add hasChangedNodeDescription state and setHasChangedNodeDescription function to track changes in node description and update parent component 📝 (edit-name-description-node.spec.ts): add wait for sidebar custom component button and timeout to improve test reliability * ✨ (group.spec.ts): Update click event on "title-Group" element to improve user interaction 🐛 (group.spec.ts): Fix click event on "save-name-description-button" element to properly save changes 🐛 (general-bugs-save-changes-on-node.spec.ts): Increase timeout for selectors to prevent test failures due to slow loading 🐛 (general-bugs-save-changes-on-node.spec.ts): Fix random value generation to ensure consistent length 🐛 (general-bugs-save-changes-on-node.spec.ts): Fix click event on "add-component-button-text-output" element to add component correctly 🐛 (general-bugs-save-changes-on-node.spec.ts): Fix timeout for selector to prevent test failures due to slow loading 🐛 (general-bugs-save-changes-on-node.spec.ts): Fix verifyTextareaValue function to properly verify textarea values --------- Co-authored-by: anovazzi1 <otavio2204@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
6b17240d75
commit
8531e1b58d
14 changed files with 465 additions and 127 deletions
|
|
@ -16,6 +16,10 @@ export default function NodeDescription({
|
|||
inputClassName,
|
||||
mdClassName,
|
||||
style,
|
||||
editNameDescription,
|
||||
setEditNameDescription,
|
||||
stickyNote,
|
||||
setHasChangedNodeDescription,
|
||||
}: {
|
||||
description?: string;
|
||||
selected?: boolean;
|
||||
|
|
@ -26,8 +30,11 @@ export default function NodeDescription({
|
|||
inputClassName?: string;
|
||||
mdClassName?: string;
|
||||
style?: React.CSSProperties;
|
||||
editNameDescription: boolean;
|
||||
setEditNameDescription?: (value: boolean) => void;
|
||||
stickyNote?: boolean;
|
||||
setHasChangedNodeDescription?: (value: boolean) => void;
|
||||
}) {
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
const [nodeDescription, setNodeDescription] = useState<string>(
|
||||
description ?? "",
|
||||
);
|
||||
|
|
@ -36,6 +43,12 @@ export default function NodeDescription({
|
|||
const overflowRef = useRef<HTMLDivElement>(null);
|
||||
const [hasScroll, sethasScroll] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && editNameDescription) {
|
||||
takeSnapshot();
|
||||
}
|
||||
}, [editNameDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
//timeout to wait for the dom to update
|
||||
setTimeout(() => {
|
||||
|
|
@ -49,13 +62,7 @@ export default function NodeDescription({
|
|||
}
|
||||
}
|
||||
}, 200);
|
||||
}, [inputDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputDescription(false);
|
||||
}
|
||||
}, [selected]);
|
||||
}, [editNameDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(description ?? "");
|
||||
|
|
@ -80,55 +87,81 @@ export default function NodeDescription({
|
|||
[description, emptyPlaceholder, mdClassName],
|
||||
);
|
||||
|
||||
const handleBlurFn = () => {
|
||||
setNodeDescription(nodeDescription);
|
||||
setNode(nodeId, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
description: nodeDescription,
|
||||
},
|
||||
},
|
||||
}));
|
||||
if (stickyNote) {
|
||||
setEditNameDescription?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDownFn = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
handleKeyDown(e, nodeDescription, "");
|
||||
|
||||
if (e.key === "Escape") {
|
||||
setEditNameDescription?.(false);
|
||||
setNodeDescription(description ?? "");
|
||||
|
||||
if (stickyNote) {
|
||||
setNodeDescription(nodeDescription);
|
||||
setNode(nodeId, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
description: nodeDescription,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClickFn = () => {
|
||||
if (stickyNote) {
|
||||
setEditNameDescription?.(true);
|
||||
takeSnapshot();
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setHasChangedNodeDescription?.(true);
|
||||
setNodeDescription(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
!inputDescription ? "overflow-auto" : "",
|
||||
!editNameDescription ? "overflow-auto" : "",
|
||||
hasScroll ? "nowheel" : "",
|
||||
charLimit ? "px-2 pb-4" : "",
|
||||
"w-full",
|
||||
)}
|
||||
>
|
||||
{inputDescription ? (
|
||||
{editNameDescription ? (
|
||||
<>
|
||||
<Textarea
|
||||
maxLength={charLimit}
|
||||
className={cn("nowheel h-full", inputClassName)}
|
||||
className={cn(
|
||||
"nowheel h-full w-full focus:border-primary focus:ring-0",
|
||||
inputClassName,
|
||||
)}
|
||||
autoFocus
|
||||
style={style}
|
||||
onBlur={() => {
|
||||
setInputDescription(false);
|
||||
setNodeDescription(nodeDescription);
|
||||
setNode(nodeId, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
description: nodeDescription,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}}
|
||||
onBlur={handleBlurFn}
|
||||
value={nodeDescription}
|
||||
onChange={(e) => setNodeDescription(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, nodeDescription, "");
|
||||
if (e.key === "Escape") {
|
||||
setInputDescription(false);
|
||||
setNodeDescription(nodeDescription);
|
||||
setNode(nodeId, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
description: nodeDescription,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
}}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDownFn}
|
||||
/>
|
||||
{charLimit && (nodeDescription?.length ?? 0) >= charLimit - 100 && (
|
||||
<div
|
||||
|
|
@ -150,14 +183,11 @@ export default function NodeDescription({
|
|||
data-testid="generic-node-desc"
|
||||
ref={overflowRef}
|
||||
className={cn(
|
||||
"nodoubleclick generic-node-desc-text h-full cursor-text text-[13px] text-muted-foreground word-break-break-word",
|
||||
"nodoubleclick generic-node-desc-text h-full cursor-grab text-[13px] text-muted-foreground word-break-break-word",
|
||||
description === "" || !description ? "font-light italic" : "",
|
||||
placeholderClassName,
|
||||
)}
|
||||
onDoubleClick={(e) => {
|
||||
setInputDescription(true);
|
||||
takeSnapshot();
|
||||
}}
|
||||
onDoubleClick={handleDoubleClickFn}
|
||||
>
|
||||
{renderedDescription}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ export default function NodeName({
|
|||
validationStatus,
|
||||
isOutdated,
|
||||
beta,
|
||||
editNameDescription,
|
||||
toggleEditNameDescription,
|
||||
setHasChangedNodeDescription,
|
||||
}: {
|
||||
display_name?: string;
|
||||
selected?: boolean;
|
||||
|
|
@ -21,70 +24,83 @@ export default function NodeName({
|
|||
validationStatus: VertexBuildTypeAPI | null;
|
||||
isOutdated: boolean;
|
||||
beta: boolean;
|
||||
editNameDescription: boolean;
|
||||
toggleEditNameDescription: () => void;
|
||||
setHasChangedNodeDescription: (hasChanged: boolean) => void;
|
||||
}) {
|
||||
const [inputName, setInputName] = useState(false);
|
||||
const [nodeName, setNodeName] = useState<string>(display_name ?? "");
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputName(false);
|
||||
if (selected && editNameDescription) {
|
||||
takeSnapshot();
|
||||
}
|
||||
}, [selected]);
|
||||
}, [editNameDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(display_name ?? "");
|
||||
}, [display_name]);
|
||||
|
||||
return inputName ? (
|
||||
const handleBlur = () => {
|
||||
if (nodeName?.trim() !== "") {
|
||||
setNodeName(nodeName);
|
||||
setNode(nodeId, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
display_name: nodeName,
|
||||
},
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setNodeName(display_name ?? "");
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
handleBlur();
|
||||
toggleEditNameDescription();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setNodeName(display_name ?? "");
|
||||
toggleEditNameDescription();
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNodeName(e.target.value);
|
||||
setHasChangedNodeDescription(true);
|
||||
};
|
||||
|
||||
return editNameDescription ? (
|
||||
<div className="m-[1px] w-full">
|
||||
<Input
|
||||
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 ?? "");
|
||||
}
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
value={nodeName}
|
||||
autoFocus
|
||||
onChange={(e) => setNodeName(e.target.value)}
|
||||
onChange={onChange}
|
||||
data-testid={`input-title-${display_name}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="py-1"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="group flex w-full items-center gap-1">
|
||||
<div
|
||||
onDoubleClick={(event) => {
|
||||
if (!showNode) {
|
||||
return;
|
||||
}
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
data-testid={"title-" + display_name}
|
||||
className={cn(
|
||||
"nodoubleclick w-full truncate font-medium text-primary",
|
||||
showNode ? "cursor-text" : "cursor-default",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex cursor-grab items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"max-w-44 truncate text-[14px]",
|
||||
"max-w-44 cursor-grab truncate text-[14px]",
|
||||
validationStatus?.data?.duration && "max-w-36",
|
||||
beta && "max-w-36",
|
||||
validationStatus?.data?.duration && beta && "max-w-20",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ const HideShowButton = memo(
|
|||
unstyled
|
||||
onClick={onClick}
|
||||
data-testid={`input-inspection-${title.toLowerCase()}`}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ShadTooltip
|
||||
content={disabled ? null : hidden ? "Show output" : "Hide output"}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ export default function NodeStatus({
|
|||
onClick={handleClickRun}
|
||||
>
|
||||
{showNode && (
|
||||
<Button unstyled className="group">
|
||||
<Button unstyled className="nodrag group">
|
||||
<div data-testid={`button_run_` + display_name.toLowerCase()}>
|
||||
<IconComponent
|
||||
name={iconName}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ 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 { useUpdateNodeInternals } from "@xyflow/react";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
ICON_STROKE_WIDTH,
|
||||
TOOLTIP_HIDDEN_OUTPUTS,
|
||||
TOOLTIP_OPEN_HIDDEN_OUTPUTS,
|
||||
} from "../../constants/constants";
|
||||
|
|
@ -20,7 +21,9 @@ import { NodeDataType } from "../../types/flow";
|
|||
import { checkHasToolMode } from "../../utils/reactflowUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
|
||||
import { useAlternate } from "@/shared/hooks/use-alternate";
|
||||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
import { useChangeOnUnfocus } from "../../shared/hooks/use-change-on-unfocus";
|
||||
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
|
||||
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
||||
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||
|
|
@ -54,8 +57,8 @@ const HiddenOutputsButton = memo(
|
|||
>
|
||||
<ForwardedIconComponent
|
||||
name={showHiddenOutputs ? "ChevronsDownUp" : "ChevronsUpDown"}
|
||||
strokeWidth={1.5}
|
||||
className="h-4 w-4 text-placeholder-foreground group-hover:text-foreground"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className="icon-size text-placeholder-foreground group-hover:text-foreground"
|
||||
/>
|
||||
</Button>
|
||||
),
|
||||
|
|
@ -99,6 +102,9 @@ function GenericNode({
|
|||
|
||||
const { mutate: validateComponentCode } = usePostValidateComponentCode();
|
||||
|
||||
const [editNameDescription, toggleEditNameDescription, set] =
|
||||
useAlternate(false);
|
||||
|
||||
const updateNodeCode = useUpdateNodeCode(
|
||||
data?.id,
|
||||
data.node!,
|
||||
|
|
@ -199,6 +205,18 @@ function GenericNode({
|
|||
[data.node?.outputs],
|
||||
);
|
||||
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useChangeOnUnfocus({
|
||||
selected,
|
||||
value: editNameDescription,
|
||||
onChange: set,
|
||||
defaultValue: false,
|
||||
shouldChangeValue: (value) => value === true,
|
||||
nodeRef,
|
||||
callback: toggleEditNameDescription,
|
||||
});
|
||||
|
||||
const renderOutputs = useCallback(
|
||||
(outputs, key?: string) => {
|
||||
return outputs?.map((output, idx) => (
|
||||
|
|
@ -231,29 +249,78 @@ function GenericNode({
|
|||
[data.node?.outputs],
|
||||
);
|
||||
|
||||
const [hasChangedNodeDescription, setHasChangedNodeDescription] =
|
||||
useState(false);
|
||||
|
||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||
return selected ? (
|
||||
<div className={cn("absolute -top-12 left-1/2 z-50 -translate-x-1/2")}>
|
||||
<NodeToolbarComponent
|
||||
data={data}
|
||||
deleteNode={(id) => {
|
||||
takeSnapshot();
|
||||
deleteNode(id);
|
||||
}}
|
||||
setShowNode={(show) => {
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
data: { ...old.data, showNode: show },
|
||||
}));
|
||||
}}
|
||||
numberOfOutputHandles={shownOutputs.length ?? 0}
|
||||
showNode={showNode}
|
||||
openAdvancedModal={false}
|
||||
onCloseAdvancedModal={() => {}}
|
||||
updateNode={handleUpdateCode}
|
||||
isOutdated={isOutdated && isUserEdited}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -top-12 left-1/2 z-50 -translate-x-1/2",
|
||||
"transform transition-all duration-300 ease-out",
|
||||
)}
|
||||
>
|
||||
<NodeToolbarComponent
|
||||
data={data}
|
||||
deleteNode={(id) => {
|
||||
takeSnapshot();
|
||||
deleteNode(id);
|
||||
}}
|
||||
setShowNode={(show) => {
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
data: { ...old.data, showNode: show },
|
||||
}));
|
||||
}}
|
||||
numberOfOutputHandles={shownOutputs.length ?? 0}
|
||||
showNode={showNode}
|
||||
openAdvancedModal={false}
|
||||
onCloseAdvancedModal={() => {}}
|
||||
updateNode={handleUpdateCode}
|
||||
isOutdated={isOutdated && isUserEdited}
|
||||
/>
|
||||
</div>
|
||||
<div className="-z-10">
|
||||
<Button
|
||||
unstyled
|
||||
onClick={() => {
|
||||
toggleEditNameDescription();
|
||||
setHasChangedNodeDescription(false);
|
||||
}}
|
||||
className={cn(
|
||||
"nodrag absolute left-1/2 z-50 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md",
|
||||
"transform transition-all duration-300 ease-out",
|
||||
showNode
|
||||
? "top-2 translate-x-[10.4rem]"
|
||||
: "top-0 translate-x-[6.4rem]",
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
? "bg-accent-emerald"
|
||||
: "bg-zinc-foreground",
|
||||
)}
|
||||
data-testid={
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
? "save-name-description-button"
|
||||
: "edit-name-description-button"
|
||||
}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
? "Check"
|
||||
: "PencilLine"
|
||||
}
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className={cn(
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
? "text-accent-emerald-foreground"
|
||||
: "text-muted-foreground",
|
||||
"icon-size",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
|
@ -268,6 +335,9 @@ function GenericNode({
|
|||
isUserEdited,
|
||||
selected,
|
||||
shortcuts,
|
||||
editNameDescription,
|
||||
hasChangedNodeDescription,
|
||||
toggleEditNameDescription,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -298,6 +368,9 @@ function GenericNode({
|
|||
validationStatus={validationStatus}
|
||||
isOutdated={isOutdated}
|
||||
beta={data.node?.beta || false}
|
||||
editNameDescription={editNameDescription}
|
||||
toggleEditNameDescription={toggleEditNameDescription}
|
||||
setHasChangedNodeDescription={setHasChangedNodeDescription}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
|
|
@ -308,6 +381,9 @@ function GenericNode({
|
|||
validationStatus,
|
||||
isOutdated,
|
||||
data.node?.beta,
|
||||
editNameDescription,
|
||||
toggleEditNameDescription,
|
||||
setHasChangedNodeDescription,
|
||||
]);
|
||||
|
||||
const renderNodeStatus = useCallback(() => {
|
||||
|
|
@ -346,9 +422,19 @@ function GenericNode({
|
|||
mdClassName={"dark:prose-invert"}
|
||||
nodeId={data.id}
|
||||
selected={selected}
|
||||
editNameDescription={editNameDescription}
|
||||
setEditNameDescription={set}
|
||||
setHasChangedNodeDescription={setHasChangedNodeDescription}
|
||||
/>
|
||||
);
|
||||
}, [data.node?.description, data.id, selected]);
|
||||
}, [
|
||||
data.node?.description,
|
||||
data.id,
|
||||
selected,
|
||||
editNameDescription,
|
||||
toggleEditNameDescription,
|
||||
setHasChangedNodeDescription,
|
||||
]);
|
||||
|
||||
const renderInputParameters = useCallback(() => {
|
||||
return (
|
||||
|
|
@ -382,8 +468,8 @@ function GenericNode({
|
|||
<div className="flex h-10 w-full items-center gap-4 rounded-t-[0.69rem] bg-warning p-2 px-4 text-warning-foreground">
|
||||
<ForwardedIconComponent
|
||||
name="AlertTriangle"
|
||||
strokeWidth={1.5}
|
||||
className="h-[18px] w-[18px] shrink-0"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className="icon-size shrink-0"
|
||||
/>
|
||||
<span className="flex-1 truncate text-sm font-medium">
|
||||
{showNode && "Update Ready"}
|
||||
|
|
@ -438,7 +524,7 @@ function GenericNode({
|
|||
{showNode && <div>{renderDescription()}</div>}
|
||||
</div>
|
||||
{showNode && (
|
||||
<div className="relative">
|
||||
<div className="nopan nodelete nodrag noflow relative cursor-auto">
|
||||
<>
|
||||
{renderInputParameters()}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
NOTE_NODE_MIN_HEIGHT,
|
||||
NOTE_NODE_MIN_WIDTH,
|
||||
} from "@/constants/constants";
|
||||
import { useAlternate } from "@/shared/hooks/use-alternate";
|
||||
import { NoteDataType } from "@/types/flow";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { NodeResizer } from "@xyflow/react";
|
||||
|
|
@ -32,6 +33,8 @@ function NoteNode({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const [editNameDescription, set] = useAlternate(false);
|
||||
|
||||
const MemoNoteToolbarComponent = useMemo(
|
||||
() =>
|
||||
selected ? (
|
||||
|
|
@ -98,6 +101,9 @@ function NoteNode({
|
|||
placeholderClassName={
|
||||
COLOR_OPTIONS[bgColor] === null ? "" : "dark:!text-background"
|
||||
}
|
||||
editNameDescription={editNameDescription}
|
||||
setEditNameDescription={set}
|
||||
stickyNote
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export const getTransformClasses = (showToolbar, showNode) => {
|
||||
if (showToolbar && showNode) return "translate-x-[10.4rem] opacity-100";
|
||||
if (!showToolbar && showNode) return "translate-x-[8rem] opacity-0";
|
||||
if (showToolbar && !showNode) return "translate-x-[6.4rem] opacity-100";
|
||||
return "translate-x-[5rem] opacity-0";
|
||||
};
|
||||
14
src/frontend/src/shared/hooks/use-alternate.tsx
Normal file
14
src/frontend/src/shared/hooks/use-alternate.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { useCallback, useState } from "react";
|
||||
|
||||
export const useAlternate = (
|
||||
initialState: boolean = false,
|
||||
): [boolean, () => void, (value: boolean) => void] => {
|
||||
const [switched, setSwitched] = useState(initialState);
|
||||
const set = useCallback((value) => setSwitched(value), []);
|
||||
|
||||
const alternate = useCallback(
|
||||
() => setSwitched((prevState) => !prevState),
|
||||
[],
|
||||
);
|
||||
return [switched, alternate, set];
|
||||
};
|
||||
49
src/frontend/src/shared/hooks/use-change-on-unfocus.tsx
Normal file
49
src/frontend/src/shared/hooks/use-change-on-unfocus.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { RefObject, useEffect } from "react";
|
||||
|
||||
interface UseChangeOnUnfocusProps<T> {
|
||||
selected?: boolean;
|
||||
value: T;
|
||||
onChange?: (value: T) => void;
|
||||
defaultValue: T;
|
||||
shouldChangeValue?: (value: T) => boolean;
|
||||
nodeRef: RefObject<HTMLDivElement>;
|
||||
callback?: () => void;
|
||||
callbackEscape?: () => void;
|
||||
}
|
||||
|
||||
export function useChangeOnUnfocus<T>({
|
||||
selected,
|
||||
value,
|
||||
onChange,
|
||||
defaultValue,
|
||||
shouldChangeValue,
|
||||
nodeRef,
|
||||
callback,
|
||||
}: UseChangeOnUnfocusProps<T>) {
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
onChange?.(defaultValue);
|
||||
}
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden && shouldChangeValue?.(value)) {
|
||||
onChange?.(defaultValue);
|
||||
callback?.();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [
|
||||
selected,
|
||||
value,
|
||||
onChange,
|
||||
defaultValue,
|
||||
shouldChangeValue,
|
||||
nodeRef,
|
||||
callback,
|
||||
]);
|
||||
}
|
||||
|
|
@ -167,6 +167,8 @@
|
|||
--tool-mode-gradient-2: #ff3276;
|
||||
|
||||
--slider-input-border: #d4d4d8;
|
||||
|
||||
--zinc-foreground: 240 5.9% 90%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -323,5 +325,7 @@
|
|||
--node-ring: 240 6% 90%;
|
||||
|
||||
--slider-input-border: #d4d4d8;
|
||||
|
||||
--zinc-foreground: 240 5.2% 33.9%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ const config = {
|
|||
"terminal-green": "hsl(var(--terminal-green))",
|
||||
"cosmic-void": "hsl(var(--cosmic-void))",
|
||||
"slider-input-border": "var(--slider-input-border)",
|
||||
"zinc-foreground": "hsl(var(--zinc-foreground))",
|
||||
},
|
||||
borderRadius: {
|
||||
lg: `var(--radius)`,
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ test.describe("group node test", () => {
|
|||
await page.getByTestId("title-OpenAI").click({ modifiers: ["Control"] });
|
||||
|
||||
await page.getByRole("button", { name: "Group" }).click();
|
||||
await page.getByTestId("title-Group").dblclick();
|
||||
await page.getByTestId("title-Group").click();
|
||||
await page.getByTestId("edit-name-description-button").click();
|
||||
await page.getByTestId("input-title-Group").first().fill("test");
|
||||
await page.getByTestId("icon-Ungroup").first().click();
|
||||
await page.getByTestId("save-name-description-button").first().click();
|
||||
await page.keyboard.press("Control+g");
|
||||
await page.getByTestId("title-OpenAI").isVisible();
|
||||
await page.getByTestId("title-Prompt").isVisible();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||
|
||||
test(
|
||||
"user should be able to edit name and description of a node",
|
||||
{ tag: ["@release", "@workspace"] },
|
||||
|
||||
async ({ page }) => {
|
||||
const randomName = Math.random().toString(36).substring(2, 15);
|
||||
const randomDescription = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
const randomName_2 = Math.random().toString(36).substring(2, 15);
|
||||
const randomDescription_2 = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
const randomName_3 = Math.random().toString(36).substring(2, 15);
|
||||
const randomDescription_3 = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
const randomName_4 = Math.random().toString(36).substring(2, 15);
|
||||
const randomDescription_4 = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("blank-flow").click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'[data-testid="sidebar-custom-component-button"]',
|
||||
{
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByTestId("sidebar-custom-component-button").click();
|
||||
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
|
||||
await page.getByTestId("edit-name-description-button").click();
|
||||
|
||||
await page.getByTestId("input-title-Custom Component").fill(randomName);
|
||||
|
||||
await page.getByTestId("textarea").fill(randomDescription);
|
||||
|
||||
await page.getByTestId("api_button_modal").click();
|
||||
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
expect(await page.getByText(randomName).count()).toBe(1);
|
||||
expect(await page.getByText(randomDescription).count()).toBe(1);
|
||||
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
|
||||
await page.getByTestId("edit-name-description-button").click();
|
||||
|
||||
await page.getByTestId(`input-title-${randomName}`).fill(randomName_2);
|
||||
|
||||
await page.getByTestId("textarea").fill(randomDescription_2);
|
||||
|
||||
await page.getByTestId("save-name-description-button").click();
|
||||
|
||||
expect(await page.getByText(randomName_2).count()).toBe(1);
|
||||
expect(await page.getByText(randomDescription_2).count()).toBe(1);
|
||||
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
|
||||
await page.getByTestId("edit-name-description-button").click();
|
||||
|
||||
await page.getByTestId(`input-title-${randomName_2}`).fill(randomName_3);
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
expect(await page.getByText(randomName_3).count()).toBe(1);
|
||||
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
|
||||
await page.getByTestId("edit-name-description-button").click();
|
||||
|
||||
await page.getByTestId(`input-title-${randomName_3}`).fill(randomName_4);
|
||||
|
||||
await page.getByTestId("textarea").fill(randomDescription_4);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
expect(await page.getByText(randomName_4).count()).toBe(1);
|
||||
|
||||
expect(await page.getByText(randomDescription_2).count()).toBe(1);
|
||||
|
||||
expect(await page.getByText(randomDescription_4).count()).toBe(0);
|
||||
|
||||
expect(await page.getByText(randomName_3).count()).toBe(0);
|
||||
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
|
||||
await page.getByTestId("edit-name-description-button").click();
|
||||
|
||||
await page.getByTestId("textarea").fill(randomDescription_3);
|
||||
|
||||
await page.getByTestId(`input-title-${randomName_4}`).fill(randomName_3);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
expect(await page.getByText(randomDescription_3).count()).toBe(1);
|
||||
|
||||
expect(await page.getByText(randomName_4).count()).toBe(1);
|
||||
|
||||
expect(await page.getByText(randomName_3).count()).toBe(0);
|
||||
|
||||
expect(await page.getByText(randomDescription_4).count()).toBe(0);
|
||||
},
|
||||
);
|
||||
|
|
@ -2,17 +2,25 @@ import { expect, Page, test } from "@playwright/test";
|
|||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||
|
||||
async function verifyTextareaValue(page: Page, value: string) {
|
||||
await page
|
||||
.getByTestId("textarea_str_input_value")
|
||||
.waitFor({ state: "visible" });
|
||||
await page.getByTestId("textarea_str_input_value").fill(value);
|
||||
|
||||
await expect(page.getByTestId("textarea_str_input_value")).toHaveValue(value);
|
||||
|
||||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page.waitForSelector('[data-testid="list-card"]', {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
await page.getByTestId("list-card").first().click();
|
||||
|
||||
await page.waitForSelector('[data-testid="textarea_str_input_value"]', {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
const inputValue = await page
|
||||
|
|
@ -26,32 +34,35 @@ test(
|
|||
{ tag: ["@release", "@components"] },
|
||||
async ({ page }) => {
|
||||
const randomValues = Array.from({ length: 4 }, () =>
|
||||
Math.random().toString(36).substring(2, 15),
|
||||
Math.random().toString(36).substring(2, 8),
|
||||
);
|
||||
|
||||
await awaitBootstrapTest(page);
|
||||
await page.getByTestId("blank-flow").click();
|
||||
|
||||
await page.waitForSelector('[data-testid="fit_view"]', {
|
||||
timeout: 100000,
|
||||
timeout: 10000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("text output");
|
||||
|
||||
await page
|
||||
.getByTestId("outputsText Output")
|
||||
.hover()
|
||||
.then(async () => {
|
||||
await page.getByTestId("add-component-button-text-output").click();
|
||||
});
|
||||
await page.getByTestId("outputsText Output").waitFor({ state: "visible" });
|
||||
await page.getByTestId("add-component-button-text-output").click();
|
||||
|
||||
await page.waitForSelector('[data-testid="title-Text Output"]', {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
// Verify each random value
|
||||
for (const value of randomValues) {
|
||||
await verifyTextareaValue(page, value);
|
||||
try {
|
||||
await verifyTextareaValue(page, value);
|
||||
} catch (error) {
|
||||
console.error(`Failed to verify value: ${value}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue