fix: restored save on change, simplified tool_mode attribution to make it work between updates (#5599)

* Fixed flowStore to run autoSave on setNode

* Fix isToolMode from GenericNode to take already set value from node

* Removed tool_mode from the usePostTemplateValue params, inserting it into inly the payload

* Removing tool_mode from usePostTemplateValue use

* Made toolMode be passed in mutateTemplate

* Refactored activateToolMode to make it work between updates

* Added autoSaveFlow dependency into useEffect

*  (general-bugs-save-changes-on-node.spec.ts): add test to verify that any changes made on the node are saved on user interaction

---------

Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
Lucas Oliveira 2025-01-09 10:58:57 -03:00 committed by GitHub
commit 0c2de6691d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 86 additions and 44 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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")) {

View file

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

View file

@ -294,32 +294,35 @@ const useFlowStore = create<FlowStoreType>((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);

View file

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