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:
parent
5e40c5f199
commit
3257c5720e
7 changed files with 119 additions and 27 deletions
|
|
@ -2,7 +2,18 @@ from enum import Enum
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
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):
|
class FormatterType(str, Enum):
|
||||||
|
|
@ -25,11 +36,12 @@ class Column(BaseModel):
|
||||||
display_name: str = Field(default="")
|
display_name: str = Field(default="")
|
||||||
sortable: bool = Field(default=True)
|
sortable: bool = Field(default=True)
|
||||||
filterable: 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
|
description: str | None = None
|
||||||
default: str | None = None
|
default: str | bool | int | float | None = None
|
||||||
disable_edit: bool = Field(default=False)
|
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)
|
hidden: bool = Field(default=False)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
|
|
@ -38,15 +50,38 @@ class Column(BaseModel):
|
||||||
self.display_name = self.name
|
self.display_name = self.name
|
||||||
return self
|
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")
|
@field_validator("formatter", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_formatter(cls, value):
|
def validate_formatter(cls, value):
|
||||||
|
if value in {"boolean", "bool"}:
|
||||||
|
value = FormatterType.boolean
|
||||||
if value in {"integer", "int", "float"}:
|
if value in {"integer", "int", "float"}:
|
||||||
value = FormatterType.number
|
value = FormatterType.number
|
||||||
if value in {"str", "string"}:
|
if value in {"str", "string"}:
|
||||||
value = FormatterType.text
|
value = FormatterType.text
|
||||||
if value == "dict":
|
if value == "dict":
|
||||||
value = FormatterType.json
|
value = FormatterType.json
|
||||||
|
if value == "date":
|
||||||
|
value = FormatterType.date
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return FormatterType(value)
|
return FormatterType(value)
|
||||||
if isinstance(value, FormatterType):
|
if isinstance(value, FormatterType):
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,10 @@ export default function TableNodeComponent({
|
||||||
pagination={!table_options?.hide_options}
|
pagination={!table_options?.hide_options}
|
||||||
addRow={addRow}
|
addRow={addRow}
|
||||||
onDelete={deleteRow}
|
onDelete={deleteRow}
|
||||||
|
gridOptions={{
|
||||||
|
ensureDomOrder: true,
|
||||||
|
suppressRowClickSelection: true,
|
||||||
|
}}
|
||||||
onDuplicate={duplicateRow}
|
onDuplicate={duplicateRow}
|
||||||
displayEmptyAlert={false}
|
displayEmptyAlert={false}
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { cn, isTimeStampString } from "@/utils/utils";
|
||||||
import { CustomCellRendererProps } from "ag-grid-react";
|
import { CustomCellRendererProps } from "ag-grid-react";
|
||||||
|
|
||||||
interface CustomCellRender extends CustomCellRendererProps {
|
interface CustomCellRender extends CustomCellRendererProps {
|
||||||
formatter?: "json" | "text";
|
formatter?: "json" | "text" | "boolean" | "number" | "undefined" | "null";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TableAutoCellRender({
|
export default function TableAutoCellRender({
|
||||||
|
|
@ -71,13 +71,32 @@ export default function TableAutoCellRender({
|
||||||
}
|
}
|
||||||
case "number":
|
case "number":
|
||||||
return <NumberReader number={value} />;
|
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:
|
default:
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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()}
|
{getCellType()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ export enum FormatterType {
|
||||||
text = "text",
|
text = "text",
|
||||||
number = "number",
|
number = "number",
|
||||||
json = "json",
|
json = "json",
|
||||||
|
boolean = "boolean",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnField {
|
export interface ColumnField {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import TableAutoCellRender from "@/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender";
|
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 useAlertStore from "@/stores/alertStore";
|
||||||
import { ColumnField, FormatterType } from "@/types/utils/functions";
|
import { ColumnField, FormatterType } from "@/types/utils/functions";
|
||||||
import { ColDef, ColGroupDef, ValueParserParams } from "ag-grid-community";
|
import { ColDef, ColGroupDef, ValueParserParams } from "ag-grid-community";
|
||||||
|
|
@ -566,7 +567,10 @@ export function FormatColumns(columns: ColumnField[]): ColDef<any>[] {
|
||||||
formatter: col.formatter,
|
formatter: col.formatter,
|
||||||
};
|
};
|
||||||
if (col.formatter !== FormatterType.text || col.edit_mode !== "inline") {
|
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.wrapText = false;
|
||||||
newCol.autoHeight = false;
|
newCol.autoHeight = false;
|
||||||
newCol.cellEditor = "agLargeTextCellEditor";
|
newCol.cellEditor = "agLargeTextCellEditor";
|
||||||
|
|
@ -574,6 +578,13 @@ export function FormatColumns(columns: ColumnField[]): ColDef<any>[] {
|
||||||
newCol.cellEditorParams = {
|
newCol.cellEditorParams = {
|
||||||
maxLength: 100000000,
|
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 {
|
} else {
|
||||||
newCol.cellRenderer = TableAutoCellRender;
|
newCol.cellRenderer = TableAutoCellRender;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,33 +113,27 @@ class CustomComponent(Component):
|
||||||
await expect(page.getByText(text).last()).toBeVisible();
|
await expect(page.getByText(text).last()).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.locator(".ag-cell-value").first().click();
|
await page.locator(".ag-cell-value").first().dblclick({
|
||||||
|
force: true,
|
||||||
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").nth(24).click();
|
await page.getByLabel("Input Editor").fill(randomText);
|
||||||
await expect(page.getByTestId("icon-Type")).toBeVisible({
|
await page.keyboard.press("Enter");
|
||||||
timeout: 2000,
|
|
||||||
|
await page.locator(".ag-cell-value").nth(12).dblclick({
|
||||||
|
force: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByPlaceholder("Empty").fill(thirdRandomText);
|
await page.getByLabel("Input Editor").fill(secondRandomText);
|
||||||
await page.getByText("Save").last().click();
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
await expect(page.getByTestId("icon-Type")).toBeHidden({
|
await page.locator(".ag-cell-value").nth(24).dblclick({
|
||||||
timeout: 2000,
|
force: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.getByLabel("Input Editor").fill(thirdRandomText);
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
expect(page.getByText(randomText)).toBeVisible();
|
expect(page.getByText(randomText)).toBeVisible();
|
||||||
expect(page.getByText(secondRandomText)).toBeVisible();
|
expect(page.getByText(secondRandomText)).toBeVisible();
|
||||||
expect(page.getByText(thirdRandomText)).toBeVisible();
|
expect(page.getByText(thirdRandomText)).toBeVisible();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue