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:
Cristhian Zanforlin Lousa 2025-02-05 11:02:57 -03:00 committed by GitHub
commit 8531e1b58d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 465 additions and 127 deletions

View file

@ -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>

View file

@ -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",

View file

@ -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"}

View file

@ -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}

View file

@ -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

View file

@ -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>

View file

@ -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";
};

View 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];
};

View 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,
]);
}

View file

@ -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%;
}
}

View file

@ -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)`,

View file

@ -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();

View file

@ -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);
},
);

View file

@ -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;
}
}
},
);