feature: Improve Table customization to enhance ux on tool mode (#5216)

* refactor: Add field validation options to TableOptions

* refactor: Add field validation options and trigger text/icon to TableMixin

* refactor: Add field validation options and trigger text/icon to TableMixin

* refactor: Add field validation options and trigger text/icon to TableMixin

* update table trigger for toolmode usage

* Refactor table trigger and field validation options

- Updated the table trigger for toolmode usage
- Added field validation options and trigger text/icon to TableMixin
- Modified TableOptions to block certain actions and hide options

* Refactor TableOptionsTypeAPI field names for blocking actions

* Refactor TableOptions default values for blocking actions

* Refactor TableOptions default values for blocking actions

* Refactor TableOptions component to include tableOptions prop

* Refactor table selection and pagination options

* Refactor TOOL_TABLE_SCHEMA to disable sorting and filtering for the "name" and "description" fields

* Refactor TableOptions to allow blocking hiding of fields

* Refactor TableModal and TableNodeComponent to include support for block hiding columns

* Refactor Column model to include support for different edit modes

* Refactor TableOptions to include support for field parsers

* Refactor TableOptions to include support for field parsers and blocking hiding of fields

* Refactor TableOptions to include support for inline editing of fields

* Refactor App.css to style large text inputs and text areas in AgGrid

* update types

* Update table modal to prevent closing the the modal while editing cell

* Refactor string manipulation utilities to support parsing and transforming strings based on specified field parsers

* add inline input support

* Refactor TextModal component to remove close button in the footer

* add field parser in context

* format code

* format code

* Add disable_edit field to Column class

* Refactor TableNodeComponent to exclude columns with disable_edit field from being editable

* [autofix.ci] apply automated fixes

* Fix casing in selector text for "Open table" in tableInputComponent tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
anovazzi1 2024-12-16 14:50:00 -03:00 committed by GitHub
commit dffc2d51cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 320 additions and 41 deletions

View file

@ -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;
}

View file

@ -17,6 +17,9 @@ export default function TableNodeComponent({
columns,
handleOnNewValue,
disabled = false,
table_options,
trigger_icon = "Table",
trigger_text = "Open Table",
}: InputProps<any[], TableComponentType>): JSX.Element {
const dataTypeDefinitions: {
[cellDataType: string]: DataTypeDefinition<any>;
@ -63,7 +66,7 @@ export default function TableNodeComponent({
const agGrid = useRef<AgGridReact>(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 (
<div
@ -119,6 +128,7 @@ export default function TableNodeComponent({
>
<div className="flex w-full items-center gap-3" data-testid={"div-" + id}>
<TableModal
tableOptions={table_options}
dataTypeDefinitions={dataTypeDefinitions}
autoSizeStrategy={{ type: "fitGridWidth", defaultMinWidth: 100 }}
tableTitle={tableTitle}
@ -127,10 +137,10 @@ export default function TableNodeComponent({
onSelectionChanged={(event: SelectionChangedEvent) => {
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 }}
>
<Button
disabled={disabled}
@ -148,8 +159,11 @@ export default function TableNodeComponent({
(disabled ? "pointer-events-none cursor-not-allowed" : "")
}
>
<ForwardedIconComponent name="Table" className="mt-px h-4 w-4" />
<span className="font-normal">Open Table</span>
<ForwardedIconComponent
name={trigger_icon}
className="mt-px h-4 w-4"
/>
<span className="font-normal">{trigger_text}</span>
</Button>
</TableModal>
</div>

View file

@ -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 (
<div className={cn("absolute bottom-3 left-6")}>
<div className="flex items-center gap-3">
{addRow && (
{addRow && !tableOptions?.block_add && (
<div>
<ShadTooltip content={"Add a new row"}>
<Button data-testid="add-row-button" unstyled onClick={addRow}>

View file

@ -8,6 +8,7 @@ import {
} from "@/constants/constants";
import { useDarkStore } from "@/stores/darkStore";
import "@/style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
import { TableOptionsTypeAPI } from "@/types/api";
import { cn } from "@/utils/utils";
import { ColDef } from "ag-grid-community";
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
@ -36,6 +37,7 @@ export interface TableComponentProps extends AgGridReactProps {
onDelete?: () => void;
onDuplicate?: () => void;
addRow?: () => void;
tableOptions?: TableOptionsTypeAPI;
}
const TableComponent = forwardRef<
@ -55,7 +57,7 @@ const TableComponent = forwardRef<
let newCol = {
...col,
};
if (props.onSelectionChanged && index === 0) {
if (props.rowSelection && props.onSelectionChanged && index === 0) {
newCol = {
...newCol,
checkboxSelection: true,
@ -63,6 +65,17 @@ const TableComponent = forwardRef<
headerCheckboxSelectionFilteredOnly: true,
};
}
if (
(typeof props.tableOptions?.block_hide === "boolean" &&
props.tableOptions?.block_hide) ||
(Array.isArray(props.tableOptions?.block_hide) &&
props.tableOptions?.block_hide.includes(newCol.field ?? ""))
) {
newCol = {
...newCol,
lockVisible: true,
};
}
if (
(typeof props.editable === "boolean" && props.editable) ||
(Array.isArray(props.editable) &&
@ -95,6 +108,7 @@ const TableComponent = forwardRef<
}
return newCol;
});
console.log(colDef);
// @ts-ignore
const realRef: React.MutableRefObject<AgGridReact> =
useRef<AgGridReact | null>(null);
@ -170,6 +184,7 @@ const TableComponent = forwardRef<
);
}
}
console.log(colDef);
return (
<div
className={cn(
@ -206,8 +221,9 @@ const TableComponent = forwardRef<
}
}}
/>
{props.pagination && (
{!props.tableOptions?.hide_options && (
<TableOptions
tableOptions={props.tableOptions}
stateChange={columnStateChange}
hasSelection={realRef.current?.api?.getSelectedRows().length > 0}
duplicateRow={props.onDuplicate ? props.onDuplicate : undefined}

View file

@ -177,6 +177,9 @@ export function ParameterRenderComponent({
description={templateData.info || "Add or edit data"}
columns={templateData?.table_schema?.columns}
tableTitle={templateData?.display_name ?? "Table"}
table_options={templateData?.table_options}
trigger_icon={templateData?.trigger_icon}
trigger_text={templateData?.trigger_text}
/>
);
case "slider":

View file

@ -1,5 +1,5 @@
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
import { APIClassType, InputFieldType } from "@/types/api";
import { APIClassType, InputFieldType, TableOptionsTypeAPI } from "@/types/api";
import { RangeSpecType } from "@/types/components";
import { ColumnField } from "@/types/utils/functions";
@ -28,6 +28,9 @@ export type TableComponentType = {
description: string;
tableTitle: string;
columns?: ColumnField[];
table_options?: TableOptionsTypeAPI;
trigger_text?: string;
trigger_icon?: string;
};
export type FloatComponentType = {

View file

@ -3,8 +3,10 @@ import TableComponent, {
TableComponentProps,
} from "@/components/core/parameterRenderComponent/components/tableComponent";
import { Button } from "@/components/ui/button";
import { TableOptionsTypeAPI } from "@/types/api";
import { DialogClose } from "@radix-ui/react-dialog";
import { ElementRef, forwardRef, useState } from "react";
import { AgGridReact } from "ag-grid-react";
import { ElementRef, ForwardedRef, forwardRef } from "react";
import BaseModal from "../baseModal";
interface TableModalProps extends TableComponentProps {
@ -12,18 +14,28 @@ interface TableModalProps extends TableComponentProps {
description: string;
disabled?: boolean;
children: React.ReactNode;
tableOptions?: TableOptionsTypeAPI;
hideColumns?: boolean | string[];
}
const TableModal = forwardRef<
ElementRef<typeof TableComponent>,
TableModalProps
>(
const TableModal = forwardRef<AgGridReact, TableModalProps>(
(
{ tableTitle, description, children, disabled, ...props }: TableModalProps,
ref,
ref: ForwardedRef<AgGridReact>,
) => {
return (
<BaseModal disable={disabled}>
<BaseModal
onEscapeKeyDown={(e) => {
if (
(
ref as React.RefObject<AgGridReact>
)?.current?.api.getEditingCells().length
) {
e.preventDefault();
}
}}
disable={disabled}
>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description={description}>
<span className="pr-2">{tableTitle}</span>

View file

@ -70,9 +70,6 @@ export default function TextModal({
Save
</Button>
)}
<Button className="flex gap-2 px-3" onClick={() => setOpen(false)}>
Close
</Button>
</div>
</BaseModal.Footer>
</BaseModal>

View file

@ -290,3 +290,40 @@ export type useMutationFunctionType<
"mutationFn" | "mutationKey"
>,
) => UseMutationResult<Data, Error, Variables>;
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<FieldParserType | { [key: string]: FieldParserType }>;
};

View file

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

View file

@ -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;
}

View file

@ -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>): string {
return classes.filter(Boolean).join(" ");
@ -515,6 +521,20 @@ export function FormatColumns(columns: ColumnField[]): ColDef<any>[] {
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<any>[] {
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<any>[] {
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
};

View file

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