fix: prompt template not being saved on advanced modal (#2488)

* Fixed dbvalue on table node cell renderer

* Added Change Advanced hook

* Added Handle New Value hook

* Added Handle Node Class hook

* Added Node Class handler to TableNodeCellRender

* Removed internal state of EditNode, added internal state for NodeClass and made the rows and columns be updated when NodeClass changes

* Added NodeClass as dependencies on useMemo to update columns when NodeClass changes

* Fixed advanced not changing the node

* feat: updating tests without save btn

* Added Close button on editNode

---------

Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
Co-authored-by: anovazzi1 <otavio2204@gmail.com>
This commit is contained in:
Lucas Oliveira 2024-07-03 14:55:52 -03:00 committed by GitHub
commit fe21f90aec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 262 additions and 88 deletions

View file

@ -2,6 +2,7 @@ import { CustomCellRendererProps } from "ag-grid-react";
import { cloneDeep } from "lodash";
import { useState } from "react";
import useFlowStore from "../../../../stores/flowStore";
import { APIClassType } from "../../../../types/api";
import {
convertObjToArray,
convertValuesToNumbers,
@ -24,24 +25,31 @@ import ToggleShadComponent from "../../../toggleShadComponent";
export default function TableNodeCellRender({
node: { data },
value: { value, nodeClass, handleOnNewValue: handleOnNewValueNode },
value: {
value,
nodeClass,
handleOnNewValue: handleOnNewValueNode,
handleNodeClass,
},
}: CustomCellRendererProps) {
const handleOnNewValue = (newValue: any, name: string, dbValue?: boolean) => {
handleOnNewValueNode(newValue, name, dbValue);
setTemplateData((old) => {
let newData = cloneDeep(old);
newData.value = newValue;
if (dbValue) {
if (dbValue !== undefined) {
newData.load_from_db = newValue;
}
return newData;
});
setTemplateValue(newValue);
};
const setNodeClass = (value: APIClassType, code?: string, type?: string) => {
handleNodeClass(value, templateData.key, code, type);
};
const [templateValue, setTemplateValue] = useState(value);
const [templateData, setTemplateData] = useState(data);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const edges = useFlowStore((state) => state.edges);
@ -220,9 +228,7 @@ export default function TableNodeCellRender({
editNode={true}
disabled={disabled}
nodeClass={nodeClass}
setNodeClass={(value) => {
nodeClass = value;
}}
setNodeClass={setNodeClass}
value={templateValue ?? ""}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateData.key);
@ -237,9 +243,7 @@ export default function TableNodeCellRender({
<CodeAreaComponent
readonly={nodeClass.flow && templateData.dynamic ? true : false}
dynamic={templateData.dynamic ?? false}
setNodeClass={(value) => {
nodeClass = value;
}}
setNodeClass={setNodeClass}
nodeClass={nodeClass}
disabled={disabled}
editNode={true}

View file

@ -1,12 +1,19 @@
import { ColDef, ValueGetterParams } from "ag-grid-community";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender";
import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender";
import { APIClassType } from "../../../types/api";
import { NodeDataType } from "../../../types/flow";
const useColumnDefs = (
myData: NodeDataType,
nodeClass: APIClassType,
handleOnNewValue: (newValue: any, name: string, setDb?: boolean) => void,
handleNodeClass: (
newNodeClass: APIClassType,
name: string,
code: string,
type?: string,
) => void,
changeAdvanced: (n: string) => void,
open: boolean,
) => {
@ -46,8 +53,9 @@ const useColumnDefs = (
valueGetter: (params: ValueGetterParams) => {
return {
value: params.data.value,
nodeClass: myData.node,
nodeClass: nodeClass,
handleOnNewValue: handleOnNewValue,
handleNodeClass: handleNodeClass,
};
},
minWidth: 340,
@ -75,7 +83,7 @@ const useColumnDefs = (
cellClass: "no-border",
},
],
[open, myData],
[open, nodeClass],
);
return columnDefs;

View file

@ -0,0 +1,29 @@
import { cloneDeep } from "lodash";
import { NodeDataType } from "../../../types/flow";
const useHandleChangeAdvanced = (
data: NodeDataType,
takeSnapshot: () => void,
setNode: (id: string, callback: (oldNode: any) => any) => void,
updateNodeInternals: (id: string) => void,
) => {
const handleChangeAdvanced = (name) => {
if (!data.node) return;
takeSnapshot();
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data.node.template[name].advanced =
!newNode.data.node.template[name].advanced;
return newNode;
});
updateNodeInternals(data.id);
};
return { handleChangeAdvanced };
};
export default useHandleChangeAdvanced;

View file

@ -0,0 +1,84 @@
import { cloneDeep } from "lodash";
import {
ERROR_UPDATING_COMPONENT,
TITLE_ERROR_UPDATING_COMPONENT,
} from "../../../constants/constants";
import useAlertStore from "../../../stores/alertStore";
import { ResponseErrorTypeAPI } from "../../../types/api";
import { NodeDataType } from "../../../types/flow";
const useHandleOnNewValue = (
data: NodeDataType,
takeSnapshot: () => void,
handleUpdateValues: (name: string, data: NodeDataType) => Promise<any>,
debouncedHandleUpdateValues: any,
setNode: (id: string, callback: (oldNode: any) => any) => void,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleOnNewValue = async (
newValue,
name,
dbValue,
skipSnapshot = false,
) => {
const nodeTemplate = data.node!.template[name];
const currentValue = nodeTemplate.value;
if (currentValue !== newValue && !skipSnapshot) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
currentValue !== newValue;
const typeToDebounce = nodeTemplate.type;
nodeTemplate.value = newValue;
let newTemplate;
if (shouldUpdate) {
try {
if (["int"].includes(typeToDebounce)) {
newTemplate = await handleUpdateValues(name, data);
} else {
newTemplate = await debouncedHandleUpdateValues(name, data);
}
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: TITLE_ERROR_UPDATING_COMPONENT,
list: [
responseError?.response?.data?.detail.error ??
ERROR_UPDATING_COMPONENT,
],
});
}
}
setNode(data.id, (oldNode) => {
const newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
if (dbValue !== undefined) {
newNode.data.node.template[name].load_from_db = dbValue;
}
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else {
newNode.data.node.template[name].value = newValue;
}
return newNode;
});
};
return { handleOnNewValue };
};
export default useHandleOnNewValue;

View file

@ -0,0 +1,39 @@
import { cloneDeep } from "lodash";
import { NodeDataType } from "../../../types/flow";
const useHandleNodeClass = (
data: NodeDataType,
takeSnapshot: () => void,
setNode: (id: string, callback: (oldNode: any) => any) => void,
updateNodeInternals: (id: string) => void,
) => {
const handleNodeClass = (newNodeClass, name, code, type?: string) => {
if (!data.node) return;
if (data.node!.template[name].value !== code) {
takeSnapshot();
}
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
if (type) {
newNode.data.node.template[name].type = type;
}
newNode.data.node.template[name].value = code;
return newNode;
});
updateNodeInternals(data.id);
};
return { handleNodeClass };
};
export default useHandleNodeClass;

View file

@ -1,8 +1,13 @@
import { useMemo } from "react";
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
import { APIClassType } from "../../../types/api";
import { NodeDataType } from "../../../types/flow";
const useRowData = (myData: NodeDataType, open: boolean) => {
const useRowData = (
myData: NodeDataType,
nodeClass: APIClassType,
open: boolean,
) => {
const rowData = useMemo(() => {
return Object.keys(myData.node!.template)
.filter((key: string) => {
@ -25,7 +30,7 @@ const useRowData = (myData: NodeDataType, open: boolean) => {
id: key,
};
});
}, [open, myData]);
}, [open, nodeClass]);
return rowData;
};

View file

@ -1,13 +1,23 @@
import { ColDef, GridApi } from "ag-grid-community";
import { cloneDeep } from "lodash";
import { forwardRef, useEffect, useRef, useState } from "react";
import { ColDef } from "ag-grid-community";
import { forwardRef, useState } from "react";
import { useUpdateNodeInternals } from "reactflow";
import TableComponent from "../../components/tableComponent";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { APIClassType } from "../../types/api";
import { NodeDataType } from "../../types/flow";
import {
debouncedHandleUpdateValues,
handleUpdateValues,
} from "../../utils/parameterUtils";
import BaseModal from "../baseModal";
import useColumnDefs from "./hooks/use-column-defs";
import useHandleChangeAdvanced from "./hooks/use-handle-change-advanced";
import useHandleOnNewValue from "./hooks/use-handle-new-value";
import useHandleNodeClass from "./hooks/use-handle-node-class";
import useRowData from "./hooks/use-row-data";
const EditNodeModal = forwardRef(
@ -25,41 +35,50 @@ const EditNodeModal = forwardRef(
},
ref,
) => {
const myData = useRef(cloneDeep(data));
const isDark = useDarkStore((state) => state.dark);
const setNode = useFlowStore((state) => state.setNode);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const updateNodeInternals = useUpdateNodeInternals();
function changeAdvanced(n) {
myData.current.node!.template[n].advanced =
!myData.current.node!.template[n]?.advanced;
}
const handleOnNewValue = (newValue: any, key: string, setDb?: boolean) => {
myData.current.node!.template[key].value = newValue;
if (setDb) {
myData.current.node!.template[key].load_from_db = newValue;
}
};
const rowData = useRowData(data, open);
const columnDefs: ColDef[] = useColumnDefs(
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
data,
handleOnNewValue,
changeAdvanced,
open,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
);
const [gridApi, setGridApi] = useState<GridApi | null>(null);
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
data,
takeSnapshot,
setNode,
updateNodeInternals,
);
useEffect(() => {
if (gridApi && open) {
myData.current = cloneDeep(data);
gridApi.refreshCells();
}
}, [gridApi, open]);
const [nodeClass, setNodeClass] = useState<APIClassType>(data.node!);
const handleNodeClass = (
newNodeClass: APIClassType,
name: string,
code: string,
type?: string,
) => {
handleNodeClassHook(newNodeClass, name, code, type);
setNodeClass(newNodeClass);
};
const { handleChangeAdvanced: handleChangeAdvancedHook } =
useHandleChangeAdvanced(data, takeSnapshot, setNode, updateNodeInternals);
const rowData = useRowData(data, nodeClass, open);
const columnDefs: ColDef[] = useColumnDefs(
nodeClass,
handleOnNewValueHook,
handleNodeClass,
handleChangeAdvancedHook,
open,
);
return (
<BaseModal key={data.id} open={open} setOpen={setOpen}>
@ -80,9 +99,6 @@ const EditNodeModal = forwardRef(
{nodeLength > 0 && (
<TableComponent
key={"editNode"}
onGridReady={(params) => {
setGridApi(params.api);
}}
tooltipShowDelay={0.5}
columnDefs={columnDefs}
rowData={rowData}
@ -91,22 +107,11 @@ const EditNodeModal = forwardRef(
</div>
</div>
</BaseModal.Content>
<BaseModal.Footer
submit={{
label: "Save Changes",
onClick: () => {
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: myData.current.node,
},
}));
setOpen(false);
},
}}
/>
<BaseModal.Footer>
<div className="flex w-full justify-end gap-2 pt-2">
<Button onClick={() => setOpen(false)}>Close</Button>
</div>
</BaseModal.Footer>
</BaseModal>
);
},

View file

@ -54,7 +54,7 @@ test("user must be able to send an image on chat", async ({ page }) => {
await page.getByText("Chat Input", { exact: true }).click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
await page.getByText("Save Changes").click();
await page.getByText("Close").last().click();
await page.getByText("Playground", { exact: true }).click();

View file

@ -93,12 +93,12 @@ test("user must interact with chat with Input/Output", async ({ page }) => {
await page.getByText("Chat Input", { exact: true }).click();
await page.getByTestId("advanced-button-modal").click();
await page.getByTestId("showsender_name").click();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByText("Chat Output", { exact: true }).click();
await page.getByTestId("advanced-button-modal").click();
await page.getByTestId("showsender_name").click();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page
.getByTestId("popover-anchor-input-sender_name")

View file

@ -472,7 +472,7 @@ AI:
.getByTestId("popover-anchor-input-input_message-edit")
.nth(0)
.fill("You're Happy! 🤪");
await page.getByText("Save Changes").click();
await page.getByText("Close").last().click();
await page.getByTitle("zoom in").click();
await page.getByTitle("zoom in").click();
@ -495,7 +495,7 @@ AI:
.getByTestId("popover-anchor-input-input_message-edit")
.nth(0)
.fill("You're Sad! 🥲");
await page.getByText("Save Changes").click();
await page.getByText("Close").last().click();
await page.getByTitle("fit view").click({
force: true,

View file

@ -144,7 +144,7 @@ test("dropDownComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
value = await page.getByTestId("dropdown-model_id").innerText();
if (value !== "cohere.embed-multilingual-v3") {

View file

@ -99,7 +99,7 @@ test("FloatComponent", async ({ page }) => {
await page.locator('//*[@id="showmirostat_tau"]').isChecked(),
).toBeFalsy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
const plusButtonLocator = page.locator('//*[@id="float-input"]');
const elementCount = await plusButtonLocator?.count();
@ -115,7 +115,7 @@ test("FloatComponent", async ({ page }) => {
await page.locator('//*[@id="showtemperature"]').isChecked(),
).toBeTruthy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.locator('//*[@id="float-input"]').click();
await page.locator('//*[@id="float-input"]').fill("3");

View file

@ -135,7 +135,7 @@ test("InputComponent", async ({ page }) => {
.getByTestId("popover-anchor-input-collection_name-edit")
.fill("NEW_collection_name_test_123123123!@#$&*(&%$@ÇÇÇÀõe");
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
const plusButtonLocator = page.getByTestId("input-collection_name");
const elementCount = await plusButtonLocator?.count();
@ -152,7 +152,7 @@ test("InputComponent", async ({ page }) => {
await page.locator('//*[@id="showcollection_name"]').isChecked(),
).toBeTruthy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
let value = await page
.getByTestId("popover-anchor-input-collection_name")

View file

@ -87,7 +87,7 @@ test("InputListComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByTestId("input-list-minus-btn_urls-2").isHidden();

View file

@ -51,7 +51,7 @@ test("IntComponent", async ({ page }) => {
await page.getByTestId("edit-button-modal").click();
await page.getByTestId("showmax_tokens").click();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByTestId("int-input-max_tokens").click();
await page
.getByTestId("int-input-max_tokens")
@ -172,7 +172,7 @@ test("IntComponent", async ({ page }) => {
await page.locator('//*[@id="showtemperature"]').isChecked(),
).toBeFalsy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
const plusButtonLocator = page.getByTestId("int-input-max_tokens");
const elementCount = await plusButtonLocator?.count();
@ -195,7 +195,7 @@ test("IntComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByTestId("int-input-max_tokens").click();
await page.getByTestId("int-input-max_tokens").fill("3");

View file

@ -47,7 +47,7 @@ test("KeypairListComponent", async ({ page }) => {
await page.getByTestId("showmodel_kwargs").click();
expect(await page.getByTestId("showmodel_kwargs").isChecked()).toBeTruthy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.locator('//*[@id="keypair0"]').click();
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
@ -92,7 +92,7 @@ test("KeypairListComponent", async ({ page }) => {
expect(
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked(),
).toBeFalsy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator?.count();
@ -115,7 +115,7 @@ test("KeypairListComponent", async ({ page }) => {
const elementKeyCount = await keyPairVerification?.count();
if (elementKeyCount === 1) {
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByTestId("div-generic-node").click();

View file

@ -60,7 +60,7 @@ test("LangflowShortcuts", async ({ page }) => {
await page.getByTitle("zoom out").click();
await page.getByTestId("generic-node-title-arrangement").click();
await page.keyboard.press(`${control}+Shift+A`);
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByTestId("generic-node-title-arrangement").click();
await page.keyboard.press(`${control}+d`);

View file

@ -174,5 +174,5 @@ test("NestedComponent", async ({ page }) => {
await page.locator('//*[@id="showtext_key"]').isChecked(),
).toBeTruthy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
});

View file

@ -182,7 +182,7 @@ test("PromptTemplateComponent", async ({ page }) => {
await page.locator('//*[@id="showprompt"]').click();
expect(await page.locator('//*[@id="showprompt"]').isChecked()).toBeTruthy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();

View file

@ -62,7 +62,7 @@ test("ToggleComponent", async ({ page }) => {
await page.locator('//*[@id="showload_hidden"]').isChecked(),
).toBeTruthy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.waitForSelector('[title="fit view"]', {
timeout: 100000,
@ -152,7 +152,7 @@ test("ToggleComponent", async ({ page }) => {
await page.locator('//*[@id="showuse_multithreading"]').isChecked(),
).toBeFalsy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
const plusButtonLocator = page.getByTestId("toggle-load_hidden");
const elementCount = await plusButtonLocator?.count();
@ -173,7 +173,7 @@ test("ToggleComponent", async ({ page }) => {
await page.getByTestId("toggle-edit-load_hidden").isChecked(),
).toBeTruthy();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByText("Close").last().click();
await page.getByTestId("toggle-load_hidden").click();
expect(