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:
parent
974cf2ee20
commit
8a7d7ef62f
20 changed files with 2357 additions and 4133 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue