diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index bf3792061..d721bc275 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -421,7 +421,7 @@ def build_custom_component_template( raise HTTPException( status_code=400, detail={ - "error": (f"Something went wrong while building the custom component. Hints: {str(exc)}"), + "error": (f"Error building Component: {str(exc)}"), "traceback": traceback.format_exc(), }, ) from exc diff --git a/src/backend/base/langflow/graph/vertex/base.py b/src/backend/base/langflow/graph/vertex/base.py index a8cf5a771..7635057a1 100644 --- a/src/backend/base/langflow/graph/vertex/base.py +++ b/src/backend/base/langflow/graph/vertex/base.py @@ -7,6 +7,7 @@ import types from enum import Enum from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Mapping, Optional, Set +import pandas as pd from loguru import logger from langflow.exceptions.component import ComponentBuildException @@ -373,6 +374,13 @@ class Vertex: params[field_name] = val elif isinstance(val, str): params[field_name] = val != "" + elif field.get("type") == "table" and val is not None: + # check if the value is a list of dicts + # if it is, create a pandas dataframe from it + if isinstance(val, list) and all(isinstance(item, dict) for item in val): + params[field_name] = pd.DataFrame(val) + else: + raise ValueError(f"Invalid value type {type(val)} for field {field_name}") elif val is not None and val != "": params[field_name] = val diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py index 49ef05a69..01283ad46 100644 --- a/src/backend/base/langflow/inputs/__init__.py +++ b/src/backend/base/langflow/inputs/__init__.py @@ -15,6 +15,7 @@ from .inputs import ( PromptInput, SecretStrInput, StrInput, + TableInput, ) __all__ = [ @@ -34,4 +35,5 @@ __all__ = [ "SecretStrInput", "StrInput", "MessageTextInput", + "TableInput", ] diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 09451bf3d..d3a34ee2d 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_valida from langflow.field_typing.range_spec import RangeSpec from langflow.inputs.validators import CoalesceBool +from langflow.schema.table import Column, TableSchema class FieldTypes(str, Enum): @@ -18,6 +19,7 @@ class FieldTypes(str, Enum): FILE = "file" PROMPT = "prompt" OTHER = "other" + TABLE = "table" SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)] @@ -136,3 +138,16 @@ class DropDownMixin(BaseModel): class MultilineMixin(BaseModel): multiline: CoalesceBool = True + + +class TableMixin(BaseModel): + table_schema: Optional[TableSchema | list[Column]] = None + + @field_validator("table_schema") + @classmethod + def validate_table_schema(cls, v): + if isinstance(v, list) and all(isinstance(column, Column) for column in v): + return TableSchema(columns=v) + if isinstance(v, TableSchema): + return v + raise ValueError("table_schema must be a TableSchema or a list of Columns") diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index aed5f2e61..d6a5c0423 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -19,9 +19,29 @@ from .input_mixin import ( MultilineMixin, RangeMixin, SerializableFieldTypes, + TableMixin, ) +class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMixin): + field_type: Optional[SerializableFieldTypes] = FieldTypes.TABLE + is_list: bool = True + + @field_validator("value") + @classmethod + def validate_value(cls, v: Any, _info): + # Check if value is a list of dicts + if not isinstance(v, list): + raise ValueError(f"TableInput value must be a list of dictionaries or Data. Value '{v}' is not a list.") + + for item in v: + if not isinstance(item, (dict, Data)): + raise ValueError( + f"TableInput value must be a list of dictionaries or Data. Item '{item}' is not a dictionary or Data." + ) + return v + + class HandleInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin): """ Represents an Input that has a Handle to a specific type (e.g. BaseLanguageModel, BaseRetriever, etc.) @@ -338,4 +358,5 @@ InputTypes = Union[ StrInput, MessageTextInput, MessageInput, + TableInput, ] diff --git a/src/backend/base/langflow/io/__init__.py b/src/backend/base/langflow/io/__init__.py index af9d3c8da..007281904 100644 --- a/src/backend/base/langflow/io/__init__.py +++ b/src/backend/base/langflow/io/__init__.py @@ -15,6 +15,7 @@ from langflow.inputs import ( PromptInput, SecretStrInput, StrInput, + TableInput, ) from langflow.template import Output @@ -36,4 +37,5 @@ __all__ = [ "StrInput", "MessageTextInput", "Output", + "TableInput", ] diff --git a/src/backend/base/langflow/schema/table.py b/src/backend/base/langflow/schema/table.py new file mode 100644 index 000000000..a26dd737f --- /dev/null +++ b/src/backend/base/langflow/schema/table.py @@ -0,0 +1,31 @@ +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, Field, field_validator + + +class FormatterType(str, Enum): + date = "date" + text = "text" + number = "number" + json = "json" + + +class Column(BaseModel): + display_name: str + name: str + sortable: bool = Field(default=True) + filterable: bool = Field(default=True) + formatter: Optional[FormatterType | str] = None + + @field_validator("formatter") + def validate_formatter(cls, value): + if isinstance(value, str): + return FormatterType(value) + if isinstance(value, FormatterType): + return value + raise ValueError("Invalid formatter type") + + +class TableSchema(BaseModel): + columns: List[Column] diff --git a/src/backend/base/langflow/utils/validate.py b/src/backend/base/langflow/utils/validate.py index 30358eb48..7a0eef7b4 100644 --- a/src/backend/base/langflow/utils/validate.py +++ b/src/backend/base/langflow/utils/validate.py @@ -4,6 +4,8 @@ import importlib from types import FunctionType from typing import Dict, List, Optional, Union +from pydantic import ValidationError + from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES @@ -168,8 +170,12 @@ def create_class(code, class_name): class_code = extract_class_code(module, class_name) compiled_class = compile_class_code(class_code) - - return build_class_constructor(compiled_class, exec_globals, class_name) + try: + return build_class_constructor(compiled_class, exec_globals, class_name) + except ValidationError as e: + messages = [error["msg"].split(",", 1) for error in e.errors()] + error_message = "\n".join([message[1] if len(message) > 1 else message[0] for message in messages]) + raise ValueError(error_message) from e def create_type_ignore_class(): diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 7da923df6..52ac79087 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -1,3 +1,4 @@ +import TableNodeComponent from "@/components/TableNodeComponent"; import { cloneDeep } from "lodash"; import { ReactNode, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; @@ -524,6 +525,17 @@ export default function ParameterComponent({ /> + +
+ +
+
; + } = useMemo(() => { + return { + // override `date` to handle custom date format `dd/mm/yyyy` + date: { + baseDataType: "date", + extendsDataType: "date", + valueParser: (params) => { + if (params.newValue == null) { + return null; + } + // convert from `dd/mm/yyyy` + const dateParts = params.newValue.split("/"); + return dateParts.length === 3 + ? new Date( + parseInt(dateParts[2]), + parseInt(dateParts[1]) - 1, + parseInt(dateParts[0]), + ) + : null; + }, + valueFormatter: (params) => { + let date = params.value; + if (typeof params.value === "string") { + date = new Date(params.value); + } + // convert to `dd/mm/yyyy` + return date == null + ? "‎" + : `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`; + }, + }, + number: { + baseDataType: "number", + extendsDataType: "number", + valueFormatter: (params) => + params.value == null ? "‎" : `${params.value}`, + }, + }; + }, []); + const [selectedNodes, setSelectedNodes] = useState>([]); + const agGrid = useRef(null); + const componentColumns = columns + ? columns + : generateBackendColumnsFromValue(value ?? []); + const AgColumns = FormatColumns(componentColumns); + function setAllRows() { + if (agGrid.current && !agGrid.current.api.isDestroyed()) { + const rows: any = []; + agGrid.current.api.forEachNode((node) => rows.push(node.data)); + onChange(rows); + } + } + function deleteRow() { + if (agGrid.current && selectedNodes.length > 0) { + agGrid.current.api.applyTransaction({ + remove: selectedNodes.map((node) => node.data), + }); + setSelectedNodes([]); + setAllRows(); + } + } + function duplicateRow() { + if (agGrid.current && selectedNodes.length > 0) { + const toDuplicate = selectedNodes.map((node) => cloneDeep(node.data)); + setSelectedNodes([]); + const rows: any = []; + onChange([...value, ...toDuplicate]); + } + } + function addRow() { + const newRow = {}; + componentColumns.forEach((column) => { + newRow[column.name] = null; + }); + onChange([...value, newRow]); + } + + 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, + }; + }); + + return ( +
+
+ { + setSelectedNodes(event.api.getSelectedNodes()); + }} + rowSelection="multiple" + suppressRowClickSelection={true} + editable={editable} + pagination={true} + addRow={addRow} + onDelete={deleteRow} + onDuplicate={duplicateRow} + displayEmptyAlert={false} + className="h-full w-full" + columnDefs={AgColumns} + rowData={value} + > + + +
+
+ ); +} diff --git a/src/frontend/src/components/objectRender/index.tsx b/src/frontend/src/components/objectRender/index.tsx index 36d2c59d4..c0b8e1e3b 100644 --- a/src/frontend/src/components/objectRender/index.tsx +++ b/src/frontend/src/components/objectRender/index.tsx @@ -1,12 +1,20 @@ import DictAreaModal from "../../modals/dictAreaModal"; -export default function ObjectRender({ object }: { object: any }): JSX.Element { - //TODO check object type - +export default function ObjectRender({ + object, + setValue, +}: { + object: any; + setValue?: (value: any) => void; +}): JSX.Element { + let preview = + object === null || object === undefined ? "‎" : JSON.stringify(object); + if (object === null || object === undefined) { + } return ( - +
-
{JSON.stringify(object)}
+
{preview}
); diff --git a/src/frontend/src/components/stringReaderComponent/index.tsx b/src/frontend/src/components/stringReaderComponent/index.tsx index dfcb15b54..877ed899c 100644 --- a/src/frontend/src/components/stringReaderComponent/index.tsx +++ b/src/frontend/src/components/stringReaderComponent/index.tsx @@ -1,4 +1,3 @@ -import { ColDef, Column } from "ag-grid-community"; import TextModal from "../../modals/textModal"; export default function StringReader({ @@ -6,13 +5,14 @@ export default function StringReader({ setValue, editable = false, }: { - string: string; + string: string | null; setValue: (value: string) => void; editable: boolean; }): JSX.Element { return ( - - {string} + + {/* INVISIBLE CHARACTER TO PREVENT AGgrid bug */} + {string ?? "‎"} ); } diff --git a/src/frontend/src/components/tableComponent/components/TableOptions/index.tsx b/src/frontend/src/components/tableComponent/components/TableOptions/index.tsx index 05d3ea271..f73b23bb0 100644 --- a/src/frontend/src/components/tableComponent/components/TableOptions/index.tsx +++ b/src/frontend/src/components/tableComponent/components/TableOptions/index.tsx @@ -9,35 +9,30 @@ export default function TableOptions({ deleteRow, hasSelection, stateChange, + addRow, }: { resetGrid: () => void; duplicateRow?: () => void; deleteRow?: () => void; + addRow?: () => void; hasSelection: boolean; stateChange: boolean; }): JSX.Element { return (
-
- - - -
+ {addRow && ( +
+ + + +
+ )} {duplicateRow && (
@@ -73,14 +71,35 @@ export default function TableOptions({
)}{" "} +
+ + + +
); diff --git a/src/frontend/src/components/tableComponent/components/tableAutoCellRender/index.tsx b/src/frontend/src/components/tableComponent/components/tableAutoCellRender/index.tsx index a4bad2c4c..91b3a3da2 100644 --- a/src/frontend/src/components/tableComponent/components/tableAutoCellRender/index.tsx +++ b/src/frontend/src/components/tableComponent/components/tableAutoCellRender/index.tsx @@ -6,21 +6,31 @@ import ObjectRender from "../../../objectRender"; import StringReader from "../../../stringReaderComponent"; import { Badge } from "../../../ui/badge"; +interface CustomCellRender extends CustomCellRendererProps { + formatter?: "json" | "text"; +} + export default function TableAutoCellRender({ value, setValue, colDef, -}: CustomCellRendererProps) { + formatter, +}: CustomCellRender) { function getCellType() { - switch (typeof value) { + let format: string = formatter ? formatter : typeof value; + //convert text to string to bind to the string reader + format = format === "text" ? "string" : format; + format = format === "json" ? "object" : format; + + switch (format) { case "object": - if (value === null) { - return String(value); - } else if (Array.isArray(value)) { - return ; - } else { - return ; - } + return ( + + ); + case "string": if (isTimeStampString(value)) { return ; diff --git a/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx index 83c1d1d25..8926438b9 100644 --- a/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx +++ b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx @@ -1,3 +1,4 @@ +import TableNodeComponent from "@/components/TableNodeComponent"; import { CustomCellRendererProps } from "ag-grid-react"; import { cloneDeep } from "lodash"; import { useState } from "react"; @@ -138,6 +139,19 @@ export default function TableNodeCellRender({ editNode={true} /> ); + case "table": + return ( + { + handleOnNewValue(value, templateData.key); + }} + editNode + tableTitle={templateData.display_name ?? "Table"} + value={templateValue} + /> + ); case "float": return ( diff --git a/src/frontend/src/components/tableComponent/index.tsx b/src/frontend/src/components/tableComponent/index.tsx index a75238258..de6a807d4 100644 --- a/src/frontend/src/components/tableComponent/index.tsx +++ b/src/frontend/src/components/tableComponent/index.tsx @@ -1,3 +1,4 @@ +import { ColDef } from "ag-grid-community"; import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid import { AgGridReact, AgGridReactProps } from "ag-grid-react"; @@ -16,9 +17,10 @@ import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; import TableOptions from "./components/TableOptions"; import resetGrid from "./utils/reset-grid-columns"; -interface TableComponentProps extends AgGridReactProps { - columnDefs: NonNullable; +export interface TableComponentProps extends AgGridReactProps { + columnDefs: NonNullable[]>; rowData: NonNullable; + displayEmptyAlert?: boolean; alertTitle?: string; alertDescription?: string; editable?: @@ -32,6 +34,7 @@ interface TableComponentProps extends AgGridReactProps { pagination?: boolean; onDelete?: () => void; onDuplicate?: () => void; + addRow?: () => void; } const TableComponent = forwardRef< @@ -42,6 +45,7 @@ const TableComponent = forwardRef< { alertTitle = DEFAULT_TABLE_ALERT_TITLE, alertDescription = DEFAULT_TABLE_ALERT_MSG, + displayEmptyAlert = true, ...props }, ref, @@ -62,7 +66,7 @@ const TableComponent = forwardRef< (typeof props.editable === "boolean" && props.editable) || (Array.isArray(props.editable) && props.editable.every((field) => typeof field === "string") && - (props.editable as Array).includes(newCol.headerName ?? "")) + (props.editable as Array).includes(newCol.field ?? "")) ) { newCol = { ...newCol, @@ -79,7 +83,7 @@ const TableComponent = forwardRef< onUpdate: (value: any) => void; editableCell: boolean; }> - ).find((field) => field.field === newCol.headerName); + ).find((field) => field.field === newCol.field); if (field) { newCol = { ...newCol, @@ -90,11 +94,9 @@ const TableComponent = forwardRef< } return newCol; }); - const gridRef = useRef(null); // @ts-ignore - const realRef: React.MutableRefObject = ref?.current - ? ref - : gridRef; + const realRef: React.MutableRefObject = + useRef(null); const dark = useDarkStore((state) => state.dark); const initialColumnDefs = useRef(colDef); const [columnStateChange, setColumnStateChange] = useState(false); @@ -132,7 +134,7 @@ const TableComponent = forwardRef< params.api.setGridOption("columnDefs", updatedColumnDefs); if (props.onColumnMoved) props.onColumnMoved(params); }; - if (props.rowData.length === 0) { + if (props.rowData.length === 0 && displayEmptyAlert) { return (
@@ -161,7 +163,15 @@ const TableComponent = forwardRef< }} animateRows={false} columnDefs={colDef} - ref={realRef} + ref={(node) => { + if (!node) return; + realRef.current = node; + if (typeof ref === "function") { + ref(node); + } else if (ref) { + ref.current = node; + } + }} onGridReady={onGridReady} onColumnMoved={onColumnMoved} onStateUpdated={(e) => { @@ -180,6 +190,7 @@ const TableComponent = forwardRef< hasSelection={realRef.current?.api?.getSelectedRows().length > 0} duplicateRow={props.onDuplicate ? props.onDuplicate : undefined} deleteRow={props.onDelete ? props.onDelete : undefined} + addRow={props.addRow ? props.addRow : undefined} resetGrid={() => { resetGrid(realRef, initialColumnDefs); setTimeout(() => { diff --git a/src/frontend/src/modals/tableModal/index.tsx b/src/frontend/src/modals/tableModal/index.tsx new file mode 100644 index 000000000..9994ce4ea --- /dev/null +++ b/src/frontend/src/modals/tableModal/index.tsx @@ -0,0 +1,45 @@ +import ForwardedIconComponent from "@/components/genericIconComponent"; +import TableComponent, { + TableComponentProps, +} from "@/components/tableComponent"; +import { Button } from "@/components/ui/button"; +import { DialogClose } from "@radix-ui/react-dialog"; +import { ElementRef, forwardRef, useState } from "react"; +import BaseModal from "../baseModal"; + +interface TableModalProps extends TableComponentProps { + tableTitle: string; + description: string; + children: React.ReactNode; +} + +const TableModal = forwardRef< + ElementRef, + TableModalProps +>(({ tableTitle, description, children, ...props }: TableModalProps, ref) => { + return ( + + {children} + + {tableTitle} + + + + + + + +
+ +
+
+
+
+ ); +}); + +export default TableModal; diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 7c5c60579..ef785b5de 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -1,3 +1,4 @@ +import { ColDef } from "ag-grid-community"; import { ReactElement, ReactNode, SetStateAction } from "react"; import { ReactFlowJsonObject } from "reactflow"; import { InputOutput } from "../../constants/enums"; @@ -9,6 +10,7 @@ import { } from "../api"; import { ChatMessageType } from "../chat"; import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index"; +import { ColumnField } from "../utils/functions"; import { sourceHandleType, targetHandleType } from "./../flow/index"; export type InputComponentType = { name?: string; @@ -134,6 +136,16 @@ export type TextAreaComponentType = { readonly?: boolean; }; +export type TableComponentType = { + description: string; + tableTitle: string; + onChange: (value: any[]) => void; + value: any[]; + editNode?: boolean; + id?: string; + columns?: ColumnField[]; +}; + export type outputComponentType = { types: string[]; selected: string; diff --git a/src/frontend/src/types/utils/functions.ts b/src/frontend/src/types/utils/functions.ts index 59ef9f2d7..077d7c767 100644 --- a/src/frontend/src/types/utils/functions.ts +++ b/src/frontend/src/types/utils/functions.ts @@ -8,3 +8,18 @@ export type getCodesObjProps = { }; export type getCodesObjReturn = Array<{ name: string; code: string }>; + +export enum FormatterType { + date = "date", + text = "text", + number = "number", + json = "json", +} + +export type ColumnField = { + display_name: string; + name: string; + sortable: boolean; + filterable: boolean; + formatter?: FormatterType; +}; diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index f6c8bfd42..f5389f3b9 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -134,6 +134,7 @@ import { SquarePen, Store, SunIcon, + Table, TerminalIcon, TerminalSquare, TextCursorInput, @@ -582,6 +583,7 @@ export const nodeIconsLucide: iconsType = { PGVector: CpuIcon, Confluence: ConfluenceIcon, FreezeAll: freezeAllIcon, + Table: Table, AIML: AIMLIcon, "AI/ML": AIMLIcon, }; diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 8ea79fd7a..634b24cec 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -1,3 +1,4 @@ +import { ColumnField, FormatterType } from "@/types/utils/functions"; import { ColDef, ColGroupDef } from "ag-grid-community"; import clsx, { ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -357,7 +358,7 @@ export function extractColumnsFromRows( rows: object[], mode: "intersection" | "union", excludeColumns?: Array, -): (ColDef | ColGroupDef)[] { +): ColDef[] { let columnsKeys: { [key: string]: ColDef | ColGroupDef } = {}; if (rows.length === 0) { return []; @@ -471,3 +472,62 @@ export function isEndpointNameValid(name: string, maxLength: number): boolean { name.length === 0 ); } + +export function FormatColumns(columns: ColumnField[]): ColDef[] { + if (!columns) return []; + const basic_types = new Set(["date", "number"]); + const colDefs = columns.map((col, index) => { + let newCol: ColDef = { + headerName: col.display_name, + field: col.name, + sortable: col.sortable, + filter: col.filterable, + }; + if (col.formatter) { + if (basic_types.has(col.formatter)) { + newCol.cellDataType = col.formatter; + } else { + newCol.cellRendererParams = { + formatter: col.formatter, + }; + newCol.cellRenderer = TableAutoCellRender; + } + } + return newCol; + }); + + return colDefs; +} + +export function generateBackendColumnsFromValue(rows: Object[]): ColumnField[] { + const columns = extractColumnsFromRows(rows, "union"); + return columns.map((column) => { + const newColumn: ColumnField = { + name: column.field ?? "", + display_name: column.headerName ?? "", + sortable: true, + filterable: true, + }; + if (rows[0] && rows[0][column.field ?? ""]) { + const value = rows[0][column.field ?? ""] as any; + if (typeof value === "string") { + if (isTimeStampString(value)) { + newColumn.formatter = FormatterType.date; + } else { + newColumn.formatter = FormatterType.text; + } + } else if (typeof value === "object" && value !== null) { + // Check if the object is a Date object + if ( + Object.prototype.toString.call(value) === "[object Date]" || + value instanceof Date + ) { + newColumn.formatter = FormatterType.date; + } else { + newColumn.formatter = FormatterType.json; + } + } + } + return newColumn; + }); +}