fix: Notes nodes width resets to default on page reload (#6025)

* fix note node size

* ♻️ (NoteNode/index.tsx): remove commented out code for debounced resize handler to improve code readability and maintainability

* feat: update NoteNode to resize and persist dimensions in state

* 🐛 (generalBugs-shard-10.spec.ts): fix clicking on the last element with the text "openai" instead of the first one to match the intended behavior
✨ (generalBugs-shard-10.spec.ts): refactor test to use forEach loop for better readability and accuracy in comparing text contents

* 🐛 (general-bugs-save-changes-on-node.spec.ts): add 500ms delay to fix flakiness in tests related to clicking elements and waiting for selectors to appear

* ✨ (appHeaderComponent/index.tsx): add data-testid attribute to app header for testing purposes
📝 (general-bugs-save-changes-on-node.spec.ts): update verifyTextareaValue function to accept an additional parameter flowName for better test coverage and clarity
📝 (general-bugs-save-changes-on-node.spec.ts): add functionality to input and fill a random flow name in the test scenario for improved test coverage and reliability

---------

Co-authored-by: anovazzi1 <otavio2204@gmail.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
Cristhian Zanforlin Lousa 2025-02-05 15:53:50 -03:00 • committed by GitHub
commit 8a7d7ef62f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 2357 additions and 4133 deletions

View file

@ -4,12 +4,20 @@ import {
NOTE_NODE_MIN_WIDTH,
} from "@/constants/constants";
import { useAlternate } from "@/shared/hooks/use-alternate";
import useFlowStore from "@/stores/flowStore";
import { NoteDataType } from "@/types/flow";
import { cn } from "@/utils/utils";
import { NodeResizer } from "@xyflow/react";
import { debounce } from "lodash";
import { useEffect, useMemo, useRef, useState } from "react";
import NodeDescription from "../GenericNode/components/NodeDescription";
import NoteToolbarComponent from "./NoteToolbarComponent";
const NOTE_NODE_PADDING = 25;
const CHAR_LIMIT = 2500;
const DEFAULT_WIDTH = 324;
const DEFAULT_HEIGHT = 324;
function NoteNode({
data,
selected,
@ -22,16 +30,68 @@ function NoteNode({
(key) => key === data.node?.template.backgroundColor,
) ?? Object.keys(COLOR_OPTIONS)[0];
const nodeDiv = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({ width: 0, height: 0 });
//tricky to start the description with the right size
const [size, setSize] = useState({
width: DEFAULT_WIDTH - NOTE_NODE_PADDING,
height: DEFAULT_HEIGHT - NOTE_NODE_PADDING,
});
const [resizedNote, setResizedNote] = useState(false);
const currentFlow = useFlowStore((state) => state.currentFlow);
const setNode = useFlowStore((state) => state.setNode);
const [isResizing, setIsResizing] = useState(false);
const nodeData = useMemo(
() => currentFlow?.data?.nodes.find((node) => node.id === data.id),
[currentFlow, data.id],
);
const nodeDataWidth = useMemo(
() => nodeData?.width ?? DEFAULT_WIDTH,
[nodeData?.width],
);
const nodeDataHeight = useMemo(
() => nodeData?.height ?? DEFAULT_HEIGHT,
[nodeData?.height],
);
const dataId = useMemo(() => data.id, [data.id]);
const dataDescription = useMemo(
() => data.node?.description,
[data.node?.description],
);
const debouncedResize = useMemo(
() =>
debounce((width: number, height: number) => {
setSize({
width: width - NOTE_NODE_PADDING,
height: height - NOTE_NODE_PADDING,
});
setNode(data.id, (node) => {
return {
...node,
width: width,
height: height,
};
});
}, 5),
[],
);
useEffect(() => {
if (nodeDiv.current) {
if (nodeData && !resizedNote && nodeDataWidth > 0 && nodeDataHeight > 0) {
setSize({
width: nodeDiv.current.offsetWidth - 25,
height: nodeDiv.current.offsetHeight - 25,
width: nodeDataWidth - NOTE_NODE_PADDING,
height: nodeDataHeight - NOTE_NODE_PADDING,
});
} else if (!nodeData && nodeDiv.current) {
const currentWidth = nodeDiv.current.offsetWidth || DEFAULT_WIDTH;
const currentHeight = nodeDiv.current.offsetHeight || DEFAULT_HEIGHT;
setSize({
width: Math.max(currentWidth, DEFAULT_WIDTH) - NOTE_NODE_PADDING,
height: Math.max(currentHeight, DEFAULT_HEIGHT) - NOTE_NODE_PADDING,
});
}
}, []);
}, [nodeData, nodeDataWidth, nodeDataHeight, resizedNote]);
const [editNameDescription, set] = useAlternate(false);
@ -49,25 +109,35 @@ function NoteNode({
return (
<>
<NodeResizer
minWidth={NOTE_NODE_MIN_WIDTH}
minHeight={NOTE_NODE_MIN_HEIGHT}
minWidth={Math.max(DEFAULT_WIDTH, NOTE_NODE_MIN_WIDTH)}
minHeight={Math.max(DEFAULT_HEIGHT, NOTE_NODE_MIN_HEIGHT)}
onResize={(_, params) => {
const { width, height } = params;
setSize({ width: width - 25, height: height - 25 });
debouncedResize(width, height);
}}
isVisible={selected}
lineClassName="!border !border-muted-foreground"
onResizeStart={() => {
setResizedNote(true);
setIsResizing(true);
}}
onResizeEnd={() => {
setIsResizing(false);
debouncedResize.flush();
}}
/>
<div
data-testid="note_node"
style={{
minWidth: NOTE_NODE_MIN_WIDTH,
minHeight: NOTE_NODE_MIN_HEIGHT,
minWidth: Math.max(DEFAULT_WIDTH, NOTE_NODE_MIN_WIDTH),
minHeight: Math.max(DEFAULT_HEIGHT, NOTE_NODE_MIN_HEIGHT),
backgroundColor: COLOR_OPTIONS[bgColor] ?? "#00000000",
}}
ref={nodeDiv}
className={cn(
"relative flex h-full w-full flex-col gap-3 rounded-xl p-3 transition-all",
"relative flex h-full w-full flex-col gap-3 rounded-xl p-3",
"transition-all duration-200 ease-in-out",
!isResizing && "transition-transform",
COLOR_OPTIONS[bgColor] !== null &&
`border ${!selected && "-z-50 shadow-sm"}`,
)}
@ -76,9 +146,14 @@ function NoteNode({
<div
style={{
width: size.width,
height: size.height,
height: "100%",
display: "flex",
overflow: "hidden",
}}
className={cn(
"transition-all duration-200 ease-in-out",
!isResizing && "transition-[width,height]",
)}
>
<NodeDescription
inputClassName={cn(
@ -93,10 +168,10 @@ function NoteNode({
: "dark:!text-background",
)}
style={{ backgroundColor: COLOR_OPTIONS[bgColor] ?? "#00000000" }}
charLimit={2500}
nodeId={data.id}
charLimit={CHAR_LIMIT}
nodeId={dataId}
selected={selected}
description={data.node?.description}
description={dataDescription}
emptyPlaceholder="Double-click to start typing or enter Markdown..."
placeholderClassName={
COLOR_OPTIONS[bgColor] === null ? "" : "dark:!text-background"

View file

@ -50,7 +50,10 @@ export default function AppHeader(): JSX.Element {
useResetDismissUpdateAll();
return (
<div className="flex h-[62px] w-full items-center justify-between gap-2 border-b px-5 py-2.5 dark:bg-background">
<div
className="flex h-[62px] w-full items-center justify-between gap-2 border-b px-5 py-2.5 dark:bg-background"
data-testid="app-header"
>
{/* Left Section */}
<div
className={`flex items-center gap-2`}

View file

@ -1,7 +1,11 @@
import { expect, Page, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
async function verifyTextareaValue(page: Page, value: string) {
async function verifyTextareaValue(
page: Page,
value: string,
flowName: string,
) {
await page
.getByTestId("textarea_str_input_value")
.waitFor({ state: "visible" });
@ -9,6 +13,8 @@ async function verifyTextareaValue(page: Page, value: string) {
await expect(page.getByTestId("textarea_str_input_value")).toHaveValue(value);
await page.waitForTimeout(500);
await page.getByTestId("icon-ChevronLeft").first().click();
await page.waitForSelector('[data-testid="list-card"]', {
@ -16,13 +22,15 @@ async function verifyTextareaValue(page: Page, value: string) {
state: "visible",
});
await page.getByTestId("list-card").first().click();
await page.waitForTimeout(500);
await page.getByText(flowName).first().click();
await page.waitForSelector('[data-testid="textarea_str_input_value"]', {
timeout: 5000,
state: "visible",
});
await page.waitForTimeout(500);
const inputValue = await page
.getByTestId("textarea_str_input_value")
.inputValue();
@ -37,6 +45,8 @@ test(
Math.random().toString(36).substring(2, 8),
);
const randomFlowName = Math.random().toString(36).substring(2, 8);
await awaitBootstrapTest(page);
await page.getByTestId("blank-flow").click();
@ -45,6 +55,12 @@ test(
state: "visible",
});
await page.getByTestId("input-flow-name").click();
await page.getByTestId("input-flow-name").fill(randomFlowName);
await page.keyboard.press("Enter");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
@ -56,9 +72,11 @@ test(
state: "visible",
});
await page.getByTestId("app-header").first().click();
for (const value of randomValues) {
try {
await verifyTextareaValue(page, value);
await verifyTextareaValue(page, value, randomFlowName);
} catch (error) {
console.error(`Failed to verify value: ${value}`, error);
throw error;

View file

@ -29,7 +29,7 @@ test(
await page.getByTestId("fit_view").click();
await page.getByText("openai").first().click();
await page.getByText("openai").last().click();
await page.keyboard.press("Delete");
//connection 1
@ -94,8 +94,8 @@ test(
.getByTestId("div-chat-message")
.allTextContents();
const concatAllText2 = textContents2.join(" ");
expect(concatAllText2).toBe(concatAllText);
textContents2.forEach((text) => {
expect(text).toBe(concatAllText);
});
},
);