refactor: Improve Reactflow wrapping around toolbars, canvas controls, etc (#8130)

* update the wrapping of the reactflow

* [autofix.ci] apply automated fixes

* test button fix

* [autofix.ci] apply automated fixes

* selector fix

* fix slash

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Mike Fortman 2025-05-21 13:46:46 -05:00 committed by GitHub
commit 3889d5ff3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 101 additions and 106 deletions

View file

@ -109,7 +109,7 @@ const CanvasControls = ({ children }) => {
return (
<Panel
data-testid="canvas_controls"
className="react-flow__controls !m-2 flex !flex-row gap-1.5 rounded-md border border-secondary-hover bg-background fill-foreground stroke-foreground p-1.5 text-primary shadow [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent"
className="react-flow__controls !left-auto !m-2 flex !flex-row gap-1.5 rounded-md border border-secondary-hover bg-background fill-foreground stroke-foreground p-1.5 text-primary shadow [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent"
position="bottom-left"
>
{/* Zoom In */}

View file

@ -49,7 +49,7 @@ const FlowToolbar = memo(function FlowToolbar(): JSX.Element {
return (
<>
<Panel className="!m-2" position="top-right">
<Panel className="!top-auto !m-2" position="top-right">
<div
className={cn(
"hover:shadow-round-btn-shadow flex h-11 items-center justify-center gap-7 rounded-md border bg-background px-1.5 shadow transition-all",

View file

@ -14,6 +14,7 @@ import { useUtilityStore } from "@/stores/utilityStore";
import { swatchColors } from "@/utils/styleUtils";
import { useCallback, useEffect, useState } from "react";
import { v5 as uuidv5 } from "uuid";
import { useShallow } from "zustand/react/shallow";
import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react";
import IconComponent from "../../components/common/genericIconComponent";
import ShadTooltip from "../../components/common/shadTooltipComponent";
@ -28,6 +29,7 @@ import BaseModal from "../baseModal";
import { ChatViewWrapper } from "./components/chat-view-wrapper";
import { SelectedViewField } from "./components/selected-view-field";
import { SidebarOpenView } from "./components/sidebar-open-view";
export default function IOModal({
children,
open,
@ -37,29 +39,33 @@ export default function IOModal({
canvasOpen,
playgroundPage,
}: IOModalPropsType): JSX.Element {
const allNodes = useFlowStore((state) => state.nodes);
const setIOModalOpen = useFlowsManagerStore((state) => state.setIOModalOpen);
const inputs = useFlowStore((state) => state.inputs).filter(
(input) => input.type !== "ChatInput",
const inputs = useFlowStore((state) => state.inputs);
const outputs = useFlowStore((state) => state.outputs);
const nodes = useFlowStore((state) => state.nodes);
const buildFlow = useFlowStore((state) => state.buildFlow);
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const isBuilding = useFlowStore((state) => state.isBuilding);
const { flowIcon, flowId, flowGradient, flowName } = useFlowStore(
useShallow((state) => ({
flowIcon: state.currentFlow?.icon,
flowId: state.currentFlow?.id,
flowGradient: state.currentFlow?.gradient,
flowName: state.currentFlow?.name,
})),
);
const chatInput = useFlowStore((state) => state.inputs).find(
(input) => input.type === "ChatInput",
);
const outputs = useFlowStore((state) => state.outputs).filter(
const filteredInputs = inputs.filter((input) => input.type !== "ChatInput");
const chatInput = inputs.find((input) => input.type === "ChatInput");
const filteredOutputs = outputs.filter(
(output) => output.type !== "ChatOutput",
);
const chatOutput = useFlowStore((state) => state.outputs).find(
(output) => output.type === "ChatOutput",
);
const nodes = useFlowStore((state) => state.nodes).filter(
const chatOutput = outputs.find((output) => output.type === "ChatOutput");
const filteredNodes = nodes.filter(
(node) =>
inputs.some((input) => input.id === node.id) ||
outputs.some((output) => output.id === node.id),
filteredOutputs.some((output) => output.id === node.id),
);
const haveChat = chatInput || chatOutput;
const [selectedTab, setSelectedTab] = useState(
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0,
);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const deleteSession = useMessagesStore((state) => state.deleteSession);
@ -68,14 +74,12 @@ export default function IOModal({
const currentFlowId = playgroundPage
? uuidv5(`${clientId}_${realFlowId}`, uuidv5.DNS)
: realFlowId;
const currentFlow = useFlowStore((state) => state.currentFlow);
const [sidebarOpen, setSidebarOpen] = useState(true);
const { mutate: deleteSessionFunction } = useDeleteMessages();
const [visibleSession, setvisibleSession] = useState<string | undefined>(
currentFlowId,
);
const flowName = useFlowStore((state) => state.currentFlow?.name);
const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground";
useEffect(() => {
@ -113,10 +117,10 @@ export default function IOModal({
function startView() {
if (!chatInput && !chatOutput) {
if (inputs.length > 0) {
return inputs[0];
if (filteredInputs.length > 0) {
return filteredInputs[0];
} else {
return outputs[0];
return filteredOutputs[0];
}
} else {
return undefined;
@ -127,10 +131,6 @@ export default function IOModal({
{ type: string; id: string } | undefined
>(startView());
const buildFlow = useFlowStore((state) => state.buildFlow);
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const isBuilding = useFlowStore((state) => state.isBuilding);
const messages = useMessagesStore((state) => state.messages);
const [sessions, setSessions] = useState<string[]>(
Array.from(
@ -184,10 +184,6 @@ export default function IOModal({
[isBuilding, setIsBuilding, chatValue, chatInput?.id, sessionId, buildFlow],
);
useEffect(() => {
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
}, [allNodes.length]);
useEffect(() => {
const sessions = new Set<string>();
messages
@ -270,9 +266,9 @@ export default function IOModal({
}, [playgroundPage, messages]);
const swatchIndex =
(currentFlow?.gradient && !isNaN(parseInt(currentFlow?.gradient))
? parseInt(currentFlow?.gradient)
: getNumberFromString(currentFlow?.gradient ?? currentFlow?.id ?? "")) %
(flowGradient && !isNaN(parseInt(flowGradient))
? parseInt(flowGradient)
: getNumberFromString(flowGradient ?? flowId ?? "")) %
swatchColors.length;
return (
@ -313,7 +309,7 @@ export default function IOModal({
)}
>
<IconComponent
name={currentFlow?.icon ?? "Workflow"}
name={flowIcon ?? "Workflow"}
className="h-3.5 w-3.5"
/>
</div>
@ -392,11 +388,11 @@ export default function IOModal({
selectedViewField={selectedViewField}
setSelectedViewField={setSelectedViewField}
haveChat={haveChat}
inputs={inputs}
outputs={outputs}
inputs={filteredInputs}
outputs={filteredOutputs}
sessions={sessions}
currentFlowId={currentFlowId}
nodes={nodes}
nodes={filteredNodes}
/>
)}
<ChatViewWrapper

View file

@ -48,7 +48,7 @@ export const MemoizedCanvasControls = memo(
export const MemoizedSidebarTrigger = memo(() => (
<Panel
className={cn(
"react-flow__controls !m-2 flex gap-1.5 rounded-md border border-secondary-hover bg-background fill-foreground stroke-foreground p-1.5 text-primary shadow transition-all duration-300 [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent",
"react-flow__controls !top-auto !m-2 flex gap-1.5 rounded-md border border-secondary-hover bg-background fill-foreground stroke-foreground p-1.5 text-primary shadow transition-all duration-300 [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent",
"pointer-events-auto opacity-100 group-data-[open=true]/sidebar-wrapper:pointer-events-none group-data-[open=true]/sidebar-wrapper:-translate-x-full group-data-[open=true]/sidebar-wrapper:opacity-0",
)}
position="top-left"

View file

@ -141,11 +141,12 @@ export default function Page({
const shadowBoxHeight = NOTE_NODE_MIN_HEIGHT * (zoomLevel || 1);
const shadowBoxBackgroundColor = COLOR_OPTIONS[Object.keys(COLOR_OPTIONS)[0]];
function handleGroupNode() {
const handleGroupNode = useCallback(() => {
takeSnapshot();
if (validateSelection(lastSelection!, edges).length === 0) {
const clonedNodes = cloneDeep(nodes);
const clonedEdges = cloneDeep(edges);
const edgesState = useFlowStore.getState().edges;
if (validateSelection(lastSelection!, edgesState).length === 0) {
const clonedNodes = cloneDeep(useFlowStore.getState().nodes);
const clonedEdges = cloneDeep(edgesState);
const clonedSelection = cloneDeep(lastSelection);
updateIds({ nodes: clonedNodes, edges: clonedEdges }, clonedSelection!);
const { newFlow } = generateFlow(
@ -169,10 +170,10 @@ export default function Page({
} else {
setErrorData({
title: INVALID_SELECTION_ERROR_ALERT,
list: validateSelection(lastSelection!, edges),
list: validateSelection(lastSelection!, edgesState),
});
}
}
}, [lastSelection, setNodes, setErrorData, takeSnapshot]);
useEffect(() => {
const handleMouseMove = (event) => {
@ -556,6 +557,27 @@ export default function Page({
<div className="h-full w-full bg-canvas" ref={reactFlowWrapper}>
{showCanvas ? (
<div id="react-flow-id" className="h-full w-full bg-canvas">
{!view && (
<>
<MemoizedCanvasControls
setIsAddingNote={setIsAddingNote}
position={position.current}
shadowBoxWidth={shadowBoxWidth}
shadowBoxHeight={shadowBoxHeight}
/>
<FlowToolbar />
</>
)}
<MemoizedSidebarTrigger />
<div className={cn(componentsToUpdate.length === 0 && "hidden")}>
<UpdateAllComponents />
</div>
<SelectionMenu
lastSelection={lastSelection}
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={handleGroupNode}
/>
<ReactFlow<AllNodeType, EdgeType>
nodes={nodes}
edges={edges}
@ -595,29 +617,6 @@ export default function Page({
onEdgeClick={handleEdgeClick}
>
<MemoizedBackground />
{!view && (
<>
<MemoizedCanvasControls
setIsAddingNote={setIsAddingNote}
position={position.current}
shadowBoxWidth={shadowBoxWidth}
shadowBoxHeight={shadowBoxHeight}
/>
<FlowToolbar />
</>
)}
<MemoizedSidebarTrigger />
<div className={cn(componentsToUpdate.length === 0 && "hidden")}>
<UpdateAllComponents />
</div>
<SelectionMenu
lastSelection={lastSelection}
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
handleGroupNode();
}}
/>
</ReactFlow>
<div
id="shadow-box"

View file

@ -33,8 +33,7 @@ test(
await page.getByText("Chat Input", { exact: true }).click();
await page.getByTestId("edit-button-modal").last().click();
await page.getByText("Close").last().click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
// Read the image file as a binary string
const filePath = "tests/assets/chain.png";

View file

@ -71,7 +71,7 @@ test(
await page.getByTestId("handle-textoutput-shownode-message-right").click();
await page.getByTestId("handle-chatoutput-noshownode-text-target").click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 100000,
});

View file

@ -35,7 +35,7 @@ test.describe("save component tests", () => {
// Now dispatch
await page.dispatchEvent(
"//*[@id='react-flow-id']/div[1]/div[1]/div",
"//*[@data-testid='rf__wrapper']/div[1]/div",
"drop",
{
dataTransfer,
@ -48,9 +48,8 @@ test.describe("save component tests", () => {
expect(true).toBeTruthy();
}
await page
.locator('//*[@id="react-flow-id"]/div[1]/div[2]/button[3]')
.click();
// Log button element
await page.getByTestId("fit_view").click();
await zoomOut(page, 2);

View file

@ -32,7 +32,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -50,7 +50,7 @@ withEventDeliveryModes(
timeout: 30000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByPlaceholder(
"No chat input variables found. Click to run your flow.",

View file

@ -38,7 +38,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -42,7 +42,7 @@ test.skip(
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForTimeout(1000);
expect(page.getByText("apple").last()).toBeVisible();
const textContents = await page

View file

@ -48,7 +48,7 @@ test.skip(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
expect(await page.locator(".markdown").count()).toBeGreaterThan(0);

View file

@ -39,7 +39,7 @@ withEventDeliveryModes(
await initialGPTsetup(page);
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 100000,

View file

@ -50,7 +50,7 @@ test(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -44,7 +44,7 @@ test(
});
// Switch to Playground
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
// Wait for the playground to be ready
const inputPlaceholder = page

View file

@ -50,7 +50,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -31,7 +31,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -39,7 +39,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -39,7 +39,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -39,7 +39,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -37,7 +37,7 @@ test.skip(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
expect(await page.locator(".markdown").count()).toBeGreaterThan(0);

View file

@ -50,7 +50,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("Add a Chat Input component to your flow to send messages.", {
exact: true,

View file

@ -71,7 +71,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector("text=default session", {
timeout: 30000,

View file

@ -41,7 +41,7 @@ withEventDeliveryModes(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })
.last()

View file

@ -254,7 +254,7 @@ withEventDeliveryModes(
timeout: 30000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 60000,
});

View file

@ -350,7 +350,7 @@ test(
await page.getByTestId("dropdown_str_model_name").click();
await page.getByTestId("gpt-4o-1-option").click();
await page.getByTestId("fit_view").click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 100000,
});

View file

@ -144,7 +144,7 @@ test.skip(
await page.getByTestId("dropdown_str_model_name").click();
await page.getByTestId("gpt-4o-1-option").click();
await page.waitForTimeout(1000);
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.getByTestId("button_run_text_output").click();
await page
.getByTestId(/^rf__node-TextOutput-[a-zA-Z0-9]+$/)
@ -167,7 +167,7 @@ test.skip(
.getByTestId(/^rf__node-TextInput-[a-zA-Z0-9]+$/)
.getByTestId("textarea_str_input_value")
.fill("This is a test, again just to be sure!");
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.getByText("Run Flow", { exact: true }).click();
await page.waitForTimeout(5000);
textInputContent = await page

View file

@ -105,7 +105,7 @@ AI:
await page.locator('//*[@id="react-flow-id"]').hover();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="button-send"]', {
timeout: 100000,

View file

@ -303,7 +303,7 @@ test(
.first()
.click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector("text=Run Flow", {
timeout: 30000,
@ -369,7 +369,9 @@ test(
timeout: 1000,
});
await page.getByTestId(`remove-file-button-${renamedTxtFile}`).click();
await page.getByText("Playground", { exact: true }).last().click();
await page
.getByRole("button", { name: "Playground", exact: true })
.click();
await page.getByTestId("icon-MoreHorizontal").last().click();
await page.getByText("Delete", { exact: true }).last().click();

View file

@ -26,7 +26,7 @@ test(
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="react-flow-id"]/div/div[2]/button[3]').click();
await page.getByTestId("fit_view").click();
await adjustScreenView(page);
await page.getByTestId("generic-node-title-arrangement").click();

View file

@ -44,7 +44,7 @@ test(
await page.getByTestId("edit-button-modal").last().click();
await page.getByText("Close").last().click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
// Read the image file as a binary string
const filePath = "tests/assets/chain.png";

View file

@ -109,7 +109,7 @@ class CustomComponent(Component):
await page.getByTestId("button_run_chat output").click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="button-stop"]', {
timeout: 30000,

View file

@ -24,7 +24,7 @@ test(
await initialGPTsetup(page);
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 100000,
@ -56,7 +56,7 @@ test(
.fill(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
);
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="button-send"]', {
timeout: 100000,
@ -83,7 +83,7 @@ test(
.nth(0)
.fill("TestSenderNameAI");
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="button-send"]', {
timeout: 100000,

View file

@ -41,7 +41,7 @@ test(
timeout: 15000,
});
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="button-send"]', {
timeout: 100000,

View file

@ -81,7 +81,7 @@ test(
.click();
await page.getByTestId("fit_view").click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 100000,
});