diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx index c2fc549be..ad5cc66f5 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx @@ -49,7 +49,6 @@ export default function NodeInputField({ node: data.node!, nodeId: data.id, parameterId: name, - tool_mode: data.node!.tool_mode ?? false, }); const setFilterEdge = useFlowStore((state) => state.setFilterEdge); const { handleNodeClass } = useHandleNodeClass(data.id); diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index eccef0d53..40bb9ebd7 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -181,8 +181,10 @@ function GenericNode({ () => data.node?.outputs?.some( (output) => output.name === "component_as_tool", - ) ?? false, - [data.node?.outputs], + ) ?? + data.node?.tool_mode ?? + false, + [data.node?.outputs, data.node?.tool_mode], ); const hasToolMode = useMemo( diff --git a/src/frontend/src/CustomNodes/helpers/mutate-template.ts b/src/frontend/src/CustomNodes/helpers/mutate-template.ts index b1bd6e0c4..91c12329d 100644 --- a/src/frontend/src/CustomNodes/helpers/mutate-template.ts +++ b/src/frontend/src/CustomNodes/helpers/mutate-template.ts @@ -20,16 +20,19 @@ export const mutateTemplate = debounce( setErrorData, parameterName?: string, callback?: () => void, + toolMode?: boolean, ) => { try { const newNode = cloneDeep(node); const newTemplate = await postTemplateValue.mutateAsync({ value: newValue, field_name: parameterName, + tool_mode: toolMode ?? node.tool_mode, }); if (newTemplate) { newNode.template = newTemplate.template; newNode.outputs = newTemplate.outputs; + newNode.tool_mode = toolMode; } setNodeClass(newNode); callback?.(); diff --git a/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts b/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts index 98b518f67..010c24214 100644 --- a/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts +++ b/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts @@ -17,7 +17,6 @@ interface IPostTemplateValueParams { node: APIClassType; nodeId: string; parameterId: string; - tool_mode: boolean; } export const usePostTemplateValue: useMutationFunctionType< @@ -25,7 +24,7 @@ export const usePostTemplateValue: useMutationFunctionType< IPostTemplateValue, APIClassType, ResponseErrorDetailAPI -> = ({ parameterId, nodeId, node, tool_mode }, options?) => { +> = ({ parameterId, nodeId, node }, options?) => { const { mutate } = UseRequestProcessor(); const postTemplateValueFn = async ( @@ -41,7 +40,7 @@ export const usePostTemplateValue: useMutationFunctionType< template: template, field: parameterId, field_value: payload.value, - tool_mode: tool_mode, + tool_mode: payload.tool_mode, }, ); diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 754b61d4a..d5f6d2c3e 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -188,7 +188,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element { useEffect(() => { useFlowStore.setState({ autoSaveFlow }); - }); + }, [autoSaveFlow]); function handleUndo(e: KeyboardEvent) { if (!isWrappedWithClass(e, "noflow")) { diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 4b7af3c23..676c24e21 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -5,7 +5,6 @@ import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; import ShadTooltip from "@/components/common/shadTooltipComponent"; import ToggleShadComponent from "@/components/core/parameterRenderComponent/components/toggleShadComponent"; import { Button } from "@/components/ui/button"; -import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow"; import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value"; import { usePostRetrieveVertexOrder } from "@/controllers/API/queries/vertex"; import useAddFlow from "@/hooks/flows/use-add-flow"; @@ -93,7 +92,6 @@ const NodeToolbarComponent = memo( }); }, }); - const updateToolMode = useFlowStore((state) => state.updateToolMode); const flowDataNodes = useMemo( () => currentFlow?.data?.nodes, @@ -114,7 +112,6 @@ const NodeToolbarComponent = memo( node: data.node!, nodeId: data.id, parameterId: "tool_mode", - tool_mode: data.node!.tool_mode ?? false, }); const isSaved = flows?.some((flow) => @@ -141,8 +138,6 @@ const NodeToolbarComponent = memo( ); const addFlow = useAddFlow(); - const { mutate: patchUpdateFlow } = usePatchUpdateFlow(); - const isMinimal = useMemo( () => countHandlesFn(data) <= 1 && numberOfOutputHandles <= 1, [data, numberOfOutputHandles], @@ -172,13 +167,8 @@ const NodeToolbarComponent = memo( }; const handleActivateToolMode = () => { - const newValue = !flowDataNodes![index]!.data.node.tool_mode; - - updateToolMode(data.id, newValue); - data.node!.tool_mode = newValue; - + const newValue = !toolMode; setToolMode(newValue); - mutateTemplate( newValue, data.node!, @@ -186,21 +176,9 @@ const NodeToolbarComponent = memo( postToolModeValue, setErrorData, "tool_mode", - () => { - currentFlow!.data!.nodes[index]!.data.node.tool_mode = newValue; - patchUpdateFlow({ - id: currentFlow?.id!, - name: currentFlow?.name!, - data: currentFlow?.data!, - description: currentFlow?.description!, - folder_id: currentFlow?.folder_id!, - endpoint_name: currentFlow?.endpoint_name!, - }); - }, + () => updateNodeInternals(data.id), + newValue, ); - - updateNodeInternals(data.id); - return newValue; }; const handleMinimize = useCallback(() => { @@ -437,6 +415,7 @@ const NodeToolbarComponent = memo( setLastCopiedSelection, paste, handleActivateToolMode, + toolMode, ], ); diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 56215a677..016cfac7f 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -294,32 +294,35 @@ const useFlowStore = create((set, get) => ({ ? change(get().nodes.find((node) => node.id === id)!) : change; - set((state) => { - const newNodes = state.nodes.map((node) => { - if (node.id === id) { - if (isUserChange) { - if ((node.data as NodeDataType).node?.frozen) { - (newChange.data as NodeDataType).node!.frozen = false; - } + const newNodes = get().nodes.map((node) => { + if (node.id === id) { + if (isUserChange) { + if ((node.data as NodeDataType).node?.frozen) { + (newChange.data as NodeDataType).node!.frozen = false; } - return newChange; } - return node; - }); + return newChange; + } + return node; + }); - const newEdges = cleanEdges(newNodes, get().edges); + const newEdges = cleanEdges(newNodes, get().edges); + set((state) => { if (callback) { // Defer the callback execution to ensure it runs after state updates are fully applied. queueMicrotask(callback); } - return { ...state, nodes: newNodes, edges: newEdges, }; }); + get().updateCurrentFlow({ nodes: newNodes, edges: newEdges }); + if (get().autoSaveFlow) { + get().autoSaveFlow!(); + } }, getNode: (id: string) => { return get().nodes.find((node) => node.id === id); diff --git a/src/frontend/tests/extended/regression/general-bugs-save-changes-on-node.spec.ts b/src/frontend/tests/extended/regression/general-bugs-save-changes-on-node.spec.ts new file mode 100644 index 000000000..adf7b1751 --- /dev/null +++ b/src/frontend/tests/extended/regression/general-bugs-save-changes-on-node.spec.ts @@ -0,0 +1,57 @@ +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").fill(value); + await page.getByTestId("icon-ChevronLeft").first().click(); + + await page.waitForSelector('[data-testid="list-card"]', { + timeout: 3000, + }); + + await page.getByTestId("list-card").first().click(); + + await page.waitForSelector('[data-testid="textarea_str_input_value"]', { + timeout: 3000, + }); + + const inputValue = await page + .getByTestId("textarea_str_input_value") + .inputValue(); + expect(inputValue).toBe(value); +} + +test( + "any changes on the node must be saved on user interaction", + { tag: ["@release", "@components"] }, + async ({ page }) => { + const randomValues = Array.from({ length: 4 }, () => + Math.random().toString(36).substring(2, 15), + ); + + await awaitBootstrapTest(page); + await page.getByTestId("blank-flow").click(); + + await page.waitForSelector('[data-testid="fit_view"]', { + timeout: 100000, + }); + 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.waitForSelector('[data-testid="title-Text Output"]', { + timeout: 3000, + }); + + // Verify each random value + for (const value of randomValues) { + await verifyTextareaValue(page, value); + } + }, +);