diff --git a/src/backend/base/langflow/base/tools/constants.py b/src/backend/base/langflow/base/tools/constants.py index ea3189701..507445c5e 100644 --- a/src/backend/base/langflow/base/tools/constants.py +++ b/src/backend/base/langflow/base/tools/constants.py @@ -1,3 +1,5 @@ +from langflow.schema.table import EditMode + TOOL_OUTPUT_NAME = "component_as_tool" TOOL_OUTPUT_DISPLAY_NAME = "Toolset" TOOLS_METADATA_INPUT_NAME = "tools_metadata" @@ -7,11 +9,17 @@ TOOL_TABLE_SCHEMA = [ "display_name": "Name", "type": "str", "description": "Specify the name of the output field.", + "sortable": False, + "filterable": False, + "edit_mode": EditMode.INLINE, }, { "name": "description", "display_name": "Description", "type": "str", "description": "Describe the purpose of the output field.", + "sortable": False, + "filterable": False, + "edit_mode": EditMode.INLINE, }, ] diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 1a9757b0a..948a9cb48 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -30,6 +30,7 @@ from langflow.schema.artifact import get_artifact_type, post_process_raw from langflow.schema.data import Data from langflow.schema.message import ErrorMessage, Message from langflow.schema.properties import Source +from langflow.schema.table import FieldParserType, TableOptions from langflow.services.tracing.schema import Log from langflow.template.field.base import UNDEFINED, Input, Output from langflow.template.frontend_node.custom_components import ComponentFrontendNode @@ -1183,8 +1184,22 @@ class Component(CustomComponent): return TableInput( name=TOOLS_METADATA_INPUT_NAME, - display_name="Tools Metadata", + info="Use the table to configure the tools.", + display_name="Toolset configuration", real_time_refresh=True, table_schema=TOOL_TABLE_SCHEMA, value=tool_data, + trigger_icon="Hammer", + trigger_text="Open toolset", + table_options=TableOptions( + block_add=True, + block_delete=True, + block_edit=True, + block_sort=True, + block_filter=True, + block_hide=True, + block_select=True, + hide_options=True, + field_parsers={"name": FieldParserType.SNAKE_CASE}, + ), ) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index b5a2098f8..3a2c85fa2 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -12,7 +12,7 @@ from pydantic import ( from langflow.field_typing.range_spec import RangeSpec from langflow.inputs.validators import CoalesceBool -from langflow.schema.table import Column, TableSchema +from langflow.schema.table import Column, TableOptions, TableSchema class FieldTypes(str, Enum): @@ -184,6 +184,9 @@ class SliderMixin(BaseModel): class TableMixin(BaseModel): table_schema: TableSchema | list[Column] | None = None + trigger_text: str = Field(default="Open table") + trigger_icon: str = Field(default="Table") + table_options: TableOptions | None = None @field_validator("table_schema") @classmethod diff --git a/src/backend/base/langflow/schema/table.py b/src/backend/base/langflow/schema/table.py index 0850369e4..efbce244e 100644 --- a/src/backend/base/langflow/schema/table.py +++ b/src/backend/base/langflow/schema/table.py @@ -13,6 +13,11 @@ class FormatterType(str, Enum): boolean = "boolean" +class EditMode(str, Enum): + MODAL = "modal" + INLINE = "inline" + + class Column(BaseModel): model_config = ConfigDict(populate_by_name=True) name: str @@ -22,6 +27,8 @@ class Column(BaseModel): formatter: FormatterType | str | None = Field(default=None, alias="type") description: str | None = None default: str | None = None + disable_edit: bool = Field(default=False) + edit_mode: EditMode | None = Field(default=EditMode.MODAL) @model_validator(mode="after") def set_display_name(self): @@ -48,3 +55,44 @@ class Column(BaseModel): class TableSchema(BaseModel): columns: list[Column] + + +class FieldValidatorType(str, Enum): + """Enum for field validation types.""" + + NO_SPACES = "no_spaces" # Prevents spaces in input + LOWERCASE = "lowercase" # Forces lowercase + UPPERCASE = "uppercase" # Forces uppercase + EMAIL = "email" # Validates email format + URL = "url" # Validates URL format + ALPHANUMERIC = "alphanumeric" # Only letters and numbers + NUMERIC = "numeric" # Only numbers + ALPHA = "alpha" # Only letters + PHONE = "phone" # Phone number format + SLUG = "slug" # URL slug format (lowercase, hyphens) + USERNAME = "username" # Alphanumeric with underscores + PASSWORD = "password" # Minimum security requirements # noqa: S105 + + +class FieldParserType(str, Enum): + """Enum for field parser types.""" + + SNAKE_CASE = "snake_case" + CAMEL_CASE = "camel_case" + PASCAL_CASE = "pascal_case" + KEBAB_CASE = "kebab_case" + LOWERCASE = "lowercase" + UPPERCASE = "uppercase" + + +class TableOptions(BaseModel): + block_add: bool = Field(default=False) + block_delete: bool = Field(default=False) + block_edit: bool = Field(default=False) + block_sort: bool = Field(default=False) + block_filter: bool = Field(default=False) + block_hide: bool | list[str] = Field(default=False) + block_select: bool = Field(default=False) + hide_options: bool = Field(default=False) + field_validators: dict[str, list[FieldValidatorType] | FieldValidatorType] | None = Field(default=None) + field_parsers: dict[str, list[FieldParserType] | FieldParserType] | None = Field(default=None) diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index f099a3aba..d45d358ba 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -197,3 +197,12 @@ code { appearance: none; margin: 0; } + +.ag-large-text-input.ag-text-area.ag-input-field { + background-color: hsl(var(--background)) !important; +} + +.ag-large-text-input.ag-text-area.ag-input-field textarea { + resize: none !important; + height: 100% !important; +} 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 cd90d60f0..516a2dadf 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx @@ -17,6 +17,9 @@ export default function TableNodeComponent({ columns, handleOnNewValue, disabled = false, + table_options, + trigger_icon = "Table", + trigger_text = "Open Table", }: InputProps): JSX.Element { const dataTypeDefinitions: { [cellDataType: string]: DataTypeDefinition; @@ -63,7 +66,7 @@ export default function TableNodeComponent({ const agGrid = useRef(null); const componentColumns = columns ? columns - : generateBackendColumnsFromValue(value ?? []); + : generateBackendColumnsFromValue(value ?? [], table_options); const AgColumns = FormatColumns(componentColumns); function setAllRows() { if (agGrid.current && !agGrid.current.api.isDestroyed()) { @@ -100,16 +103,22 @@ export default function TableNodeComponent({ function updateComponent() { setAllRows(); } - const editable = componentColumns.map((column) => { - const isCustomEdit = - column.formatter && - (column.formatter === "text" || column.formatter === "json"); - return { - field: column.name, - onUpdate: updateComponent, - editableCell: isCustomEdit ? false : true, - }; - }); + const editable = componentColumns + .map((column) => { + const isCustomEdit = + column.formatter && + ((column.formatter === "text" && column.edit_mode !== "inline") || + column.formatter === "json"); + return { + field: column.name, + onUpdate: updateComponent, + editableCell: isCustomEdit ? false : true, + }; + }) + .filter( + (col) => + columns?.find((c) => c.name === col.field)?.disable_edit !== true, + ); return (
{ setSelectedNodes(event.api.getSelectedNodes()); }} - rowSelection="multiple" + rowSelection={table_options?.block_select ? undefined : "multiple"} suppressRowClickSelection={true} editable={editable} - pagination={true} + pagination={!table_options?.hide_options} addRow={addRow} onDelete={deleteRow} onDuplicate={duplicateRow} @@ -138,6 +148,7 @@ export default function TableNodeComponent({ className="h-full w-full" columnDefs={AgColumns} rowData={value} + context={{ field_parsers: table_options?.field_parsers }} >
diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx index b9a7e5c57..8297c9fa2 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx @@ -1,6 +1,7 @@ import IconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; import { Button } from "@/components/ui/button"; +import { TableOptionsTypeAPI } from "@/types/api"; import { cn } from "@/utils/utils"; export default function TableOptions({ @@ -10,6 +11,7 @@ export default function TableOptions({ hasSelection, stateChange, addRow, + tableOptions, }: { resetGrid: () => void; duplicateRow?: () => void; @@ -17,11 +19,12 @@ export default function TableOptions({ addRow?: () => void; hasSelection: boolean; stateChange: boolean; + tableOptions?: TableOptionsTypeAPI; }): JSX.Element { return (
- {addRow && ( + {addRow && !tableOptions?.block_add && (
)} -
diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 514eadc6f..6b59ef875 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -290,3 +290,40 @@ export type useMutationFunctionType< "mutationFn" | "mutationKey" >, ) => UseMutationResult; + +export type FieldValidatorType = + | "no_spaces" + | "lowercase" + | "uppercase" + | "email" + | "url" + | "alphanumeric" + | "numeric" + | "alpha" + | "phone" + | "slug" + | "username" + | "password"; + +export type FieldParserType = + | "snake_case" + | "camel_case" + | "pascal_case" + | "kebab_case" + | "lowercase" + | "uppercase"; + +export type TableOptionsTypeAPI = { + block_add?: boolean; + block_delete?: boolean; + block_edit?: boolean; + block_sort?: boolean; + block_filter?: boolean; + block_hide?: boolean | string[]; + block_select?: boolean; + hide_options?: boolean; + field_validators?: Array< + FieldValidatorType | { [key: string]: FieldValidatorType } + >; + field_parsers?: Array; +}; diff --git a/src/frontend/src/types/utils/functions.ts b/src/frontend/src/types/utils/functions.ts index f4ea85e9e..fafb21bba 100644 --- a/src/frontend/src/types/utils/functions.ts +++ b/src/frontend/src/types/utils/functions.ts @@ -1,3 +1,5 @@ +import { FieldParserType, FieldValidatorType } from "../api"; + export type getCodesObjProps = { runCurlCode: string; webhookCurlCode: string; @@ -23,5 +25,7 @@ export interface ColumnField { filterable: boolean; formatter?: FormatterType; description?: string; + disable_edit?: boolean; default?: any; // Add this line + edit_mode?: "modal" | "inline"; } diff --git a/src/frontend/src/utils/stringManipulation.ts b/src/frontend/src/utils/stringManipulation.ts new file mode 100644 index 000000000..cdce60044 --- /dev/null +++ b/src/frontend/src/utils/stringManipulation.ts @@ -0,0 +1,74 @@ +import { FieldParserType } from "../types/api"; + +function toSnakeCase(str: string): string { + return str.trim().replace(/\s+/g, "_"); +} + +function toCamelCase(str: string): string { + return str + .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : "")) + .replace(/^[A-Z]/, (c) => c.toLowerCase()); +} + +function toPascalCase(str: string): string { + return str + .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : "")) + .replace(/^[a-z]/, (c) => c.toUpperCase()); +} + +function toKebabCase(str: string): string { + return str + .replace(/([A-Z])/g, " $1") + .trim() + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[_]+/g, "-"); +} + +function toLowerCase(str: string): string { + return str.toLowerCase(); +} + +function toUpperCase(str: string): string { + return str.toUpperCase(); +} + +export function parseString( + str: string, + parsers: FieldParserType[] | FieldParserType, +): string { + let result = str; + + let parsersArray: FieldParserType[] = []; + + if (typeof parsers === "string") { + parsersArray = [parsers]; + } else { + parsersArray = parsers; + } + + for (const parser of parsersArray) { + switch (parser) { + case "snake_case": + result = toSnakeCase(result); + break; + case "camel_case": + result = toCamelCase(result); + break; + case "pascal_case": + result = toPascalCase(result); + break; + case "kebab_case": + result = toKebabCase(result); + break; + case "lowercase": + result = toLowerCase(result); + break; + case "uppercase": + result = toUpperCase(result); + break; + } + } + + return result; +} diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 851f0ddb6..79963c095 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -1,6 +1,6 @@ import TableAutoCellRender from "@/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender"; import { ColumnField, FormatterType } from "@/types/utils/functions"; -import { ColDef, ColGroupDef } from "ag-grid-community"; +import { ColDef, ColGroupDef, ValueParserParams } from "ag-grid-community"; import clsx, { ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { @@ -9,7 +9,12 @@ import { MODAL_CLASSES, SHORTCUT_KEYS, } from "../constants/constants"; -import { APIDataType, InputFieldType, VertexDataTypeAPI } from "../types/api"; +import { + APIDataType, + InputFieldType, + TableOptionsTypeAPI, + VertexDataTypeAPI, +} from "../types/api"; import { groupedObjType, nodeGroupedObjType, @@ -18,6 +23,7 @@ import { import { NodeDataType, NodeType } from "../types/flow"; import { FlowState } from "../types/tabs"; import { isErrorLog } from "../types/utils/typeCheckingUtils"; +import { parseString } from "./stringManipulation"; export function classNames(...classes: Array): string { return classes.filter(Boolean).join(" "); @@ -515,6 +521,20 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { field: col.name, sortable: col.sortable, filter: col.filterable, + editable: !col.disable_edit, + valueParser: (params: ValueParserParams) => { + const { context, newValue, colDef } = params; + if ( + context.field_parsers && + context.field_parsers[colDef.field ?? ""] + ) { + return parseString( + newValue, + context.field_parsers[colDef.field ?? ""], + ); + } + return newValue; + }, }; if (!col.formatter) { col.formatter = FormatterType.text; @@ -525,7 +545,17 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { newCol.cellRendererParams = { formatter: col.formatter, }; - newCol.cellRenderer = TableAutoCellRender; + if (col.formatter !== FormatterType.text || col.edit_mode !== "inline") { + newCol.cellRenderer = TableAutoCellRender; + } else { + newCol.wrapText = true; + newCol.autoHeight = true; + newCol.cellEditor = "agLargeTextCellEditor"; + newCol.cellEditorPopup = true; + newCol.cellEditorParams = { + maxLength: 100000000, + }; + } } return newCol; }); @@ -533,14 +563,17 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { return colDefs; } -export function generateBackendColumnsFromValue(rows: Object[]): ColumnField[] { +export function generateBackendColumnsFromValue( + rows: Object[], + tableOptions?: TableOptionsTypeAPI, +): ColumnField[] { const columns = extractColumnsFromRows(rows, "union"); return columns.map((column) => { const newColumn: ColumnField = { name: column.field ?? "", display_name: column.headerName ?? "", - sortable: true, - filterable: true, + sortable: !tableOptions?.block_sort, + filterable: !tableOptions?.block_filter, default: null, // Initialize default to null or appropriate value }; diff --git a/src/frontend/tests/core/unit/tableInputComponent.spec.ts b/src/frontend/tests/core/unit/tableInputComponent.spec.ts index f460928b3..847cd63fc 100644 --- a/src/frontend/tests/core/unit/tableInputComponent.spec.ts +++ b/src/frontend/tests/core/unit/tableInputComponent.spec.ts @@ -83,11 +83,11 @@ class CustomComponent(Component): await page.getByText("Check & Save").last().click(); - await page.waitForSelector('text="Open Table"', { + await page.waitForSelector('text="Open table"', { timeout: 3000, }); - await page.getByText("Open Table").click(); + await page.getByText("Open table").click(); await page.waitForSelector(".ag-cell-value", { timeout: 3000, @@ -166,11 +166,11 @@ class CustomComponent(Component): await page.getByText("Close").last().click(); - await page.waitForSelector("text=Open Table", { + await page.waitForSelector("text=Open table", { timeout: 3000, }); - await page.getByText("Open Table").click(); + await page.getByText("Open table").click(); await page.waitForSelector(".ag-cell-value", { timeout: 3000,