fix: handle null and boolean correctly on table parameter (#6722)

* Added Null, Undefined and Bool rendering to table

* Handling formatter to come from the backend in table definitions

* Added validator to make type as formatter and vice versa

* Added boolean case to show badge

* Added Toggle Cell Editor

* Supress Row Click Selection on Table node component

* ADd boolean as type for formatter

* Adds Boolean formatter type to show toggle editor

* run format_backend

*  (tableInputComponent.spec.ts): update unit test for table input component to use new methods for interacting with elements and improve readability and maintainability of the test code.

---------

Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
Lucas Oliveira 2025-02-24 18:33:34 -03:00 committed by GitHub
commit 3257c5720e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 119 additions and 27 deletions

View file

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

View file

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

View file

@ -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 <NumberReader number={value} />;
case "undefined":
return "";
case "null":
return "";
case "boolean":
value =
(typeof value === "string" && value.toLowerCase() === "true") ||
value === true
? true
: false;
return (
<Badge
variant={value ? "successStatic" : "errorStatic"}
size="sq"
className="h-[18px]"
>
{String(value).toLowerCase()}
</Badge>
);
default:
return String(value);
}
}
return (
<div className="group flex h-full w-full truncate text-align-last-left">
<div className="group flex h-full w-full items-center truncate text-align-last-left">
{getCellType()}
</div>
);

View file

@ -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 (
<div className="flex h-full items-center px-2">
<ToggleShadComponent
value={value}
handleOnNewValue={(data) => {
onValueChange?.(data.value);
}}
editNode={true}
id={"toggle" + colDef?.colId + uniqueId()}
disabled={false}
/>
</div>
);
}

View file

@ -14,6 +14,7 @@ export enum FormatterType {
text = "text",
number = "number",
json = "json",
boolean = "boolean",
}
export interface ColumnField {

View file

@ -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<any>[] {
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<any>[] {
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;
}

View file

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