diff --git a/src/backend/base/langflow/schema/table.py b/src/backend/base/langflow/schema/table.py index ed3a797bb..541204551 100644 --- a/src/backend/base/langflow/schema/table.py +++ b/src/backend/base/langflow/schema/table.py @@ -2,7 +2,18 @@ from enum import Enum from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator -VALID_TYPES = ["date", "number", "text", "json", "integer", "int", "float", "str", "string", "boolean"] +VALID_TYPES = [ + "date", + "number", + "text", + "json", + "integer", + "int", + "float", + "str", + "string", + "boolean", +] class FormatterType(str, Enum): @@ -25,11 +36,12 @@ class Column(BaseModel): display_name: str = Field(default="") sortable: bool = Field(default=True) filterable: bool = Field(default=True) - formatter: FormatterType | str | None = Field(default=None, alias="type") + formatter: FormatterType | str | None = Field(default=None) + type: FormatterType | str | None = Field(default=None) description: str | None = None - default: str | None = None + default: str | bool | int | float | None = None disable_edit: bool = Field(default=False) - edit_mode: EditMode | None = Field(default=EditMode.MODAL) + edit_mode: EditMode | None = Field(default=EditMode.POPOVER) hidden: bool = Field(default=False) @model_validator(mode="after") @@ -38,15 +50,38 @@ class Column(BaseModel): self.display_name = self.name return self + @model_validator(mode="after") + def set_formatter_from_type(self): + if self.type and not self.formatter: + self.formatter = self.validate_formatter(self.type) + if self.formatter in {"boolean", "bool"}: + valid_trues = ["True", "true", "1", "yes"] + valid_falses = ["False", "false", "0", "no"] + if self.default in valid_trues: + self.default = True + if self.default in valid_falses: + self.default = False + elif self.formatter in {"integer", "int"}: + self.default = int(self.default) + elif self.formatter in {"float"}: + self.default = float(self.default) + else: + self.default = str(self.default) + return self + @field_validator("formatter", mode="before") @classmethod def validate_formatter(cls, value): + if value in {"boolean", "bool"}: + value = FormatterType.boolean if value in {"integer", "int", "float"}: value = FormatterType.number if value in {"str", "string"}: value = FormatterType.text if value == "dict": value = FormatterType.json + if value == "date": + value = FormatterType.date if isinstance(value, str): return FormatterType(value) if isinstance(value, FormatterType): diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx index 5c6c42471..925ebefba 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx @@ -179,6 +179,10 @@ export default function TableNodeComponent({ pagination={!table_options?.hide_options} addRow={addRow} onDelete={deleteRow} + gridOptions={{ + ensureDomOrder: true, + suppressRowClickSelection: true, + }} onDuplicate={duplicateRow} displayEmptyAlert={false} className="h-full w-full" diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender/index.tsx index 26d252a3b..465b31228 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender/index.tsx @@ -7,7 +7,7 @@ import { cn, isTimeStampString } from "@/utils/utils"; import { CustomCellRendererProps } from "ag-grid-react"; interface CustomCellRender extends CustomCellRendererProps { - formatter?: "json" | "text"; + formatter?: "json" | "text" | "boolean" | "number" | "undefined" | "null"; } export default function TableAutoCellRender({ @@ -71,13 +71,32 @@ export default function TableAutoCellRender({ } case "number": return ; + case "undefined": + return ""; + case "null": + return ""; + case "boolean": + value = + (typeof value === "string" && value.toLowerCase() === "true") || + value === true + ? true + : false; + return ( + + {String(value).toLowerCase()} + + ); default: return String(value); } } return ( -
+
{getCellType()}
); diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/tableToggleCellEditor/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/tableToggleCellEditor/index.tsx new file mode 100644 index 000000000..fd996fa1b --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/tableToggleCellEditor/index.tsx @@ -0,0 +1,28 @@ +import { CustomCellEditorProps } from "ag-grid-react"; +import { uniqueId } from "lodash"; +import ToggleShadComponent from "../../../toggleShadComponent"; + +export default function TableToggleCellEditor({ + value, + onValueChange, + colDef, +}: CustomCellEditorProps) { + value = + (typeof value === "string" && value.toLowerCase() === "true") || + value === true + ? true + : false; + return ( +
+ { + onValueChange?.(data.value); + }} + editNode={true} + id={"toggle" + colDef?.colId + uniqueId()} + disabled={false} + /> +
+ ); +} diff --git a/src/frontend/src/types/utils/functions.ts b/src/frontend/src/types/utils/functions.ts index ebaf4f897..17816ea74 100644 --- a/src/frontend/src/types/utils/functions.ts +++ b/src/frontend/src/types/utils/functions.ts @@ -14,6 +14,7 @@ export enum FormatterType { text = "text", number = "number", json = "json", + boolean = "boolean", } export interface ColumnField { diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index d13d3cda9..fa8e6aa96 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -1,4 +1,5 @@ import TableAutoCellRender from "@/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender"; +import TableToggleCellEditor from "@/components/core/parameterRenderComponent/components/tableComponent/components/tableToggleCellEditor"; import useAlertStore from "@/stores/alertStore"; import { ColumnField, FormatterType } from "@/types/utils/functions"; import { ColDef, ColGroupDef, ValueParserParams } from "ag-grid-community"; @@ -566,7 +567,10 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { formatter: col.formatter, }; if (col.formatter !== FormatterType.text || col.edit_mode !== "inline") { - if (col.edit_mode === "popover") { + if ( + col.edit_mode === "popover" && + col.formatter === FormatterType.text + ) { newCol.wrapText = false; newCol.autoHeight = false; newCol.cellEditor = "agLargeTextCellEditor"; @@ -574,6 +578,13 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { newCol.cellEditorParams = { maxLength: 100000000, }; + } else if (col.formatter === FormatterType.boolean) { + newCol.cellRenderer = TableAutoCellRender; + newCol.cellEditorPopup = false; + newCol.cellEditor = TableToggleCellEditor; + newCol.autoHeight = false; + newCol.cellClass = "no-border !py-2"; + newCol.type = "boolean"; } else { newCol.cellRenderer = TableAutoCellRender; } diff --git a/src/frontend/tests/core/unit/tableInputComponent.spec.ts b/src/frontend/tests/core/unit/tableInputComponent.spec.ts index 4ca87ce72..127f5fd04 100644 --- a/src/frontend/tests/core/unit/tableInputComponent.spec.ts +++ b/src/frontend/tests/core/unit/tableInputComponent.spec.ts @@ -113,33 +113,27 @@ class CustomComponent(Component): await expect(page.getByText(text).last()).toBeVisible(); } - await page.locator(".ag-cell-value").first().click(); - - await page.getByPlaceholder("Empty").fill(randomText); - await page.getByText("Save").last().click(); - await expect(page.getByTestId("icon-Type")).toBeHidden({ - timeout: 2000, - }); - await page.locator(".ag-cell-value").nth(12).click(); - - await page.getByPlaceholder("Empty").fill(secondRandomText); - await page.getByText("Save").last().click(); - await expect(page.getByTestId("icon-Type")).toBeHidden({ - timeout: 2000, + await page.locator(".ag-cell-value").first().dblclick({ + force: true, }); - await page.locator(".ag-cell-value").nth(24).click(); - await expect(page.getByTestId("icon-Type")).toBeVisible({ - timeout: 2000, + await page.getByLabel("Input Editor").fill(randomText); + await page.keyboard.press("Enter"); + + await page.locator(".ag-cell-value").nth(12).dblclick({ + force: true, }); - await page.getByPlaceholder("Empty").fill(thirdRandomText); - await page.getByText("Save").last().click(); + await page.getByLabel("Input Editor").fill(secondRandomText); + await page.keyboard.press("Enter"); - await expect(page.getByTestId("icon-Type")).toBeHidden({ - timeout: 2000, + await page.locator(".ag-cell-value").nth(24).dblclick({ + force: true, }); + await page.getByLabel("Input Editor").fill(thirdRandomText); + await page.keyboard.press("Enter"); + expect(page.getByText(randomText)).toBeVisible(); expect(page.getByText(secondRandomText)).toBeVisible(); expect(page.getByText(thirdRandomText)).toBeVisible();