Browse Source

Add datagrid editor (#1941)

Thomas Brandého 1 year ago
parent
commit
9a5579e1ef

+ 56 - 0
reflex/.templates/web/utils/helpers/dataeditor.js

@@ -0,0 +1,56 @@
+import { GridCellKind } from "@glideapps/glide-data-grid"
+
+export function getDEColumn(columns, col) {
+    let c = columns[col];
+    c.pos = col;
+    return c;
+}
+
+export function getDERow(data, row) {
+    return data[row];
+}
+
+export function locateCell(row, column) {
+    if (Array.isArray(row)) {
+        return row[column.pos];
+    } else {
+        return row[column.id];
+    }
+}
+
+export function formatCell(value, column) {
+    switch (column.type) {
+        case "int":
+        case "float":
+            return {
+                kind: GridCellKind.Number,
+                data: value,
+                displayData: value + "",
+                readonly: false,
+                allowOverlay: false
+            }
+        case "datetime":
+        // value = moment format?
+        case "str":
+            return {
+                kind: GridCellKind.Text,
+                data: value,
+                displayData: value,
+                readonly: false,
+                allowOverlay: true
+            }
+        case "bool":
+            return {
+                kind: GridCellKind.Boolean,
+                data: value,
+                readonly: false,
+                // allowOverlay: true
+            }
+        default:
+            return {
+                kind: GridCellKind.Text,
+                data: value,
+                displayData: "type not specified in column definition"
+            }
+    };
+};

+ 1 - 0
reflex/components/__init__.py

@@ -32,6 +32,7 @@ code = Code.create
 code_block = CodeBlock.create
 connection_banner = ConnectionBanner.create
 connection_modal = ConnectionModal.create
+data_editor = DataEditor.create
 data_table = DataTable.create
 divider = Divider.create
 list = List.create

+ 1 - 0
reflex/components/datadisplay/__init__.py

@@ -2,6 +2,7 @@
 
 from .badge import Badge
 from .code import Code, CodeBlock
+from .dataeditor import DataEditor
 from .datatable import DataTable
 from .divider import Divider
 from .keyboard_key import KeyboardKey

+ 408 - 0
reflex/components/datadisplay/dataeditor.py

@@ -0,0 +1,408 @@
+"""Data Editor component from glide-data-grid."""
+from __future__ import annotations
+
+from enum import Enum
+from typing import Any, Callable, Dict, List, Optional
+
+from reflex.base import Base
+from reflex.components.component import Component, NoSSRComponent
+from reflex.components.literals import LiteralRowMarker
+from reflex.utils import console, format, imports, types
+from reflex.utils.serializers import serializer
+from reflex.vars import ImportVar, Var, get_unique_variable_name
+
+
+# TODO: Fix the serialization issue for custom types.
+class GridColumnIcons(Enum):
+    """An Enum for the available icons in DataEditor."""
+
+    Array = "array"
+    AudioUri = "audio_uri"
+    Boolean = "boolean"
+    HeaderCode = "code"
+    Date = "date"
+    Email = "email"
+    Emoji = "emoji"
+    GeoDistance = "geo_distance"
+    IfThenElse = "if_then_else"
+    Image = "image"
+    JoinStrings = "join_strings"
+    Lookup = "lookup"
+    Markdown = "markdown"
+    Math = "math"
+    Number = "number"
+    Phone = "phone"
+    Reference = "reference"
+    Rollup = "rollup"
+    RowID = "row_id"
+    SingleValue = "single_value"
+    SplitString = "split_string"
+    String = "string"
+    TextTemplate = "text_template"
+    Time = "time"
+    Uri = "uri"
+    VideoUri = "video_uri"
+
+
+# @serializer
+# def serialize_gridcolumn_icon(icon: GridColumnIcons) -> str:
+#     """Serialize grid column icon.
+
+#     Args:
+#         icon: the Icon to serialize.
+
+#     Returns:
+#         The serialized value.
+#     """
+#     return "prefix" + str(icon)
+
+
+# class DataEditorColumn(Base):
+#     """Column."""
+
+#     title: str
+#     id: Optional[str] = None
+#     type_: str = "str"
+
+
+class DataEditorTheme(Base):
+    """The theme for the DataEditor component."""
+
+    accentColor: Optional[str] = None
+    accentFg: Optional[str] = None
+    accentLight: Optional[str] = None
+    baseFontStyle: Optional[str] = None
+    bgBubble: Optional[str] = None
+    bgBubbleSelected: Optional[str] = None
+    bgCell: Optional[str] = None
+    bgCellMedium: Optional[str] = None
+    bgHeader: Optional[str] = None
+    bgHeaderHasFocus: Optional[str] = None
+    bgHeaderHovered: Optional[str] = None
+    bgIconHeader: Optional[str] = None
+    bgSearchResult: Optional[str] = None
+    borderColor: Optional[str] = None
+    cellHorizontalPadding: Optional[int] = None
+    cellVerticalPadding: Optional[int] = None
+    drilldownBorder: Optional[str] = None
+    editorFontSize: Optional[str] = None
+    fgIconHeader: Optional[str] = None
+    fontFamily: Optional[str] = None
+    headerBottomBorderColor: Optional[str] = None
+    headerFontStyle: Optional[str] = None
+    horizontalBorderColor: Optional[str] = None
+    lineHeight: Optional[int] = None
+    linkColor: Optional[str] = None
+    textBubble: Optional[str] = None
+    textDark: Optional[str] = None
+    textGroupHeader: Optional[str] = None
+    textHeader: Optional[str] = None
+    textHeaderSelected: Optional[str] = None
+    textLight: Optional[str] = None
+    textMedium: Optional[str] = None
+
+
+class DataEditor(NoSSRComponent):
+    """The DataEditor Component."""
+
+    tag = "DataEditor"
+    is_default = True
+    library: str = "@glideapps/glide-data-grid@^5.3.0"
+    lib_dependencies: List[str] = ["lodash", "marked", "react-responsive-carousel"]
+
+    # Number of rows.
+    rows: Var[int]
+
+    # Headers of the columns for the data grid.
+    columns: Var[List[Dict[str, Any]]]
+
+    # The data.
+    data: Var[List[List[Any]]]
+
+    # The name of the callback used to find the data to display.
+    get_cell_content: Var[str]
+
+    # Allow selection for copying.
+    get_cell_for_selection: Var[bool]
+
+    # Allow paste.
+    on_paste: Var[bool]
+
+    # Controls the drawing of the focus ring.
+    draw_focus_ring: Var[bool]
+
+    # Enables or disables the overlay shadow when scrolling horizontally.
+    fixed_shadow_x: Var[bool]
+
+    # Enables or disables the overlay shadow when scrolling vertically.
+    fixed_shadow_y: Var[bool]
+
+    # The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
+    freeze_columns: Var[int]
+
+    # Controls the header of the group header row.
+    group_header_height: Var[int]
+
+    # Controls the height of the header row.
+    header_height: Var[int]
+
+    # Additional header icons:
+    # header_icons: Var[Any] # (TODO: must be a map of name: svg)
+
+    # The maximum width a column can be automatically sized to.
+    max_column_auto_width: Var[int]
+
+    # The maximum width a column can be resized to.
+    max_column_width: Var[int]
+
+    # The minimum width a column can be resized to.
+    min_column_width: Var[int]
+
+    # Determins the height of each row.
+    row_height: Var[int]
+
+    # Kind of row markers.
+    row_markers: Var[LiteralRowMarker]
+
+    # Changes the starting index for row markers.
+    row_marker_start_index: Var[int]
+
+    # Sets the width of row markers in pixels, if unset row markers will automatically size.
+    row_marker_width: Var[int]
+
+    # Enable horizontal smooth scrolling.
+    smooth_scroll_x: Var[bool]
+
+    # Enable vertical smooth scrolling.
+    smooth_scroll_y: Var[bool]
+
+    # Controls the drawing of the left hand vertical border of a column. If set to a boolean value it controls all borders.
+    vertical_border: Var[bool]  # TODO: support a mapping (dict[int, bool])
+
+    # Allow columns selections. ("none", "single", "multiple")
+    column_select: Var[str]
+
+    # Prevent diagonal scrolling.
+    prevent_diagonal_scrolling: Var[bool]
+
+    # Allow to scroll past the limit of the actual content on the horizontal axis.
+    overscroll_x: Var[int]
+
+    # Allow to scroll past the limit of the actual content on the vertical axis.
+    overscroll_y: Var[int]
+
+    # Initial scroll offset on the horizontal axis.
+    scroll_offset_x: Var[int]
+
+    # Initial scroll offset on the vertical axis.
+    scroll_offset_y: Var[int]
+
+    # global theme
+    theme: Var[DataEditorTheme]
+
+    def _get_imports(self):
+        return imports.merge_imports(
+            super()._get_imports(),
+            {
+                "": {
+                    ImportVar(
+                        tag=f"{format.format_library_name(self.library)}/dist/index.css"
+                    )
+                },
+                self.library: {ImportVar(tag="GridCellKind")},
+                "/utils/helpers/dataeditor.js": {
+                    ImportVar(tag=f"getDEColumn", is_default=False, install=False),
+                    ImportVar(tag=f"getDERow", is_default=False, install=False),
+                    ImportVar(tag=f"locateCell", is_default=False, install=False),
+                    ImportVar(tag=f"formatCell", is_default=False, install=False),
+                    ImportVar(tag=f"onEditCell", is_default=False, install=False),
+                },
+            },
+        )
+
+    def get_event_triggers(self) -> Dict[str, Callable]:
+        """The event triggers of the component.
+
+        Returns:
+            The dict describing the event triggers.
+        """
+
+        def edit_sig(pos, data: dict[str, Any]):
+            return [pos, data]
+
+        return {
+            "on_cell_activated": lambda pos: [pos],
+            "on_cell_clicked": lambda pos: [pos],
+            "on_cell_context_menu": lambda pos: [pos],
+            "on_cell_edited": edit_sig,
+            "on_group_header_clicked": edit_sig,
+            "on_group_header_context_menu": lambda grp_idx, data: [grp_idx, data],
+            "on_group_header_renamed": lambda idx, val: [idx, val],
+            "on_header_clicked": lambda pos: [pos],
+            "on_header_context_menu": lambda pos: [pos],
+            "on_header_menu_click": lambda col, pos: [col, pos],
+            "on_item_hovered": lambda pos: [pos],
+            "on_delete": lambda selection: [selection],
+            "on_finished_editing": lambda new_value, movement: [new_value, movement],
+            "on_row_appended": lambda: [],
+            "on_selection_cleared": lambda: [],
+        }
+
+    def _get_hooks(self) -> str | None:
+        # Define the id of the component in case multiple are used in the same page.
+        editor_id = get_unique_variable_name()
+
+        # Define the name of the getData callback associated with this component and assign to get_cell_content.
+        data_callback = f"getData_{editor_id}"
+        self.get_cell_content = Var.create(data_callback, _var_is_local=False)  # type: ignore
+
+        code = [f"function {data_callback}([col, row])" "{"]
+
+        code.extend(
+            [
+                f"  if (row < {self.data._var_full_name}.length && col < {self.columns._var_full_name}.length)"
+                " {",
+                f"    const column = getDEColumn({self.columns._var_full_name}, col);",
+                f"    const rowData = getDERow({self.data._var_full_name}, row);",
+                f"    const cellData = locateCell(rowData, column);",
+                "    return formatCell(cellData, column);",
+                "  }",
+                "  return { kind: GridCellKind.Loading};",
+            ]
+        )
+
+        code.append("}")
+
+        return "\n".join(code)
+
+    @classmethod
+    def create(cls, *children, **props) -> Component:
+        """Create the DataEditor component.
+
+        Args:
+            *children: The children of the data editor.
+            **props: The props of the data editor.
+
+        Raises:
+            ValueError: invalid input.
+
+        Returns:
+            The DataEditor component.&
+        """
+        from reflex.el.elements import Div
+
+        columns = props.get("columns", [])
+        data = props.get("data", [])
+        rows = props.get("rows", None)
+
+        # If rows is not provided, determine from data.
+        if rows is None:
+            props["rows"] = (
+                data.length()  # BaseVar.create(value=f"{data}.length()", is_local=False)
+                if isinstance(data, Var)
+                else len(data)
+            )
+
+        if not isinstance(columns, Var) and len(columns):
+            if (
+                types.is_dataframe(type(data))
+                or isinstance(data, Var)
+                and types.is_dataframe(data._var_type)
+            ):
+                raise ValueError(
+                    "Cannot pass in both a pandas dataframe and columns to the data_editor component."
+                )
+            else:
+                props["columns"] = [
+                    format.format_data_editor_column(col) for col in columns
+                ]
+
+        # Allow by default to select a region of cells in the grid.
+        props.setdefault("getCellForSelection", True)
+
+        # Disable on_paste by default if not provided.
+        props.setdefault("onPaste", False)
+
+        if props.pop("getCellContent", None) is not None:
+            console.warn(
+                "getCellContent is not user configurable, the provided value will be discarded"
+            )
+        grid = super().create(*children, **props)
+        return Div.create(
+            grid,
+            Div.create(id="portal"),
+            width=props.pop("width", "100%"),
+            height=props.pop("height", "100%"),
+        )
+
+    # def _render(self) -> Tag:
+    #     if isinstance(self.data, Var) and types.is_dataframe(self.data.type_):
+    #         self.columns = BaseVar(
+    #             name=f"{self.data.name}.columns",
+    #             type_=List[Any],
+    #             state=self.data.state,
+    #         )
+    #         self.data = BaseVar(
+    #             name=f"{self.data.name}.data",
+    #             type_=List[List[Any]],
+    #             state=self.data.state,
+    #         )
+    #     if types.is_dataframe(type(self.data)):
+    #         # If given a pandas df break up the data and columns
+    #         data = serialize(self.data)
+    #         assert isinstance(data, dict), "Serialized dataframe should be a dict."
+    #         self.columns = Var.create_safe(data["columns"])
+    #         self.data = Var.create_safe(data["data"])
+
+    #    # Render the table.
+    #    return super()._render()
+
+
+# try:
+#     pass
+
+#     # def format_dataframe_values(df: DataFrame) -> list[list[Any]]:
+#     #     """Format dataframe values to a list of lists.
+
+#     #     Args:
+#     #         df: The dataframe to format.
+
+#     #     Returns:
+#     #         The dataframe as a list of lists.
+#     #     """
+#     # return [
+#     #     [str(d) if isinstance(d, (list, tuple)) else d for d in data]
+#     #     for data in list(df.values.tolist())
+#     # ]
+#     # ...
+
+#     # @serializer
+#     # def serialize_dataframe(df: DataFrame) -> dict:
+#     #     """Serialize a pandas dataframe.
+
+#     #     Args:
+#     #         df: The dataframe to serialize.
+
+#     #     Returns:
+#     #         The serialized dataframe.
+#     #     """
+#     # return {
+#     #     "columns": df.columns.tolist(),
+#     #     "data": format_dataframe_values(df),
+#     # }
+
+# except ImportError:
+#     pass
+
+
+@serializer
+def serialize_dataeditortheme(theme: DataEditorTheme):
+    """The serializer for the data editor theme.
+
+    Args:
+        theme: The theme to serialize.
+
+    Returns:
+        The serialized theme.
+    """
+    return format.json_dumps({k: v for k, v in theme.__dict__.items() if v is not None})

+ 201 - 0
reflex/components/datadisplay/dataeditor.pyi

@@ -0,0 +1,201 @@
+"""Stub file for reflex/components/datadisplay/dataeditor.py"""
+# ------------------- DO NOT EDIT ----------------------
+# This file was generated by `scripts/pyi_generator.py`!
+# ------------------------------------------------------
+
+from typing import Any, Dict, Optional, overload, Literal, Union, List
+from reflex.vars import Var, BaseVar, ComputedVar
+from reflex.event import EventChain, EventHandler, EventSpec
+from reflex.style import Style
+from enum import Enum
+from typing import Any, Callable, Dict, List, Optional
+from reflex.base import Base
+from reflex.components.component import Component, NoSSRComponent
+from reflex.components.literals import LiteralRowMarker
+from reflex.utils import console, format, imports, types
+from reflex.utils.serializers import serializer
+from reflex.vars import ImportVar, Var, get_unique_variable_name
+
+class GridColumnIcons(Enum): ...
+
+class DataEditorTheme(Base):
+    accentColor: Optional[str]
+    accentFg: Optional[str]
+    accentLight: Optional[str]
+    baseFontStyle: Optional[str]
+    bgBubble: Optional[str]
+    bgBubbleSelected: Optional[str]
+    bgCell: Optional[str]
+    bgCellMedium: Optional[str]
+    bgHeader: Optional[str]
+    bgHeaderHasFocus: Optional[str]
+    bgHeaderHovered: Optional[str]
+    bgIconHeader: Optional[str]
+    bgSearchResult: Optional[str]
+    borderColor: Optional[str]
+    cellHorizontalPadding: Optional[int]
+    cellVerticalPadding: Optional[int]
+    drilldownBorder: Optional[str]
+    editorFontSize: Optional[str]
+    fgIconHeader: Optional[str]
+    fontFamily: Optional[str]
+    headerBottomBorderColor: Optional[str]
+    headerFontStyle: Optional[str]
+    horizontalBorderColor: Optional[str]
+    lineHeight: Optional[int]
+    linkColor: Optional[str]
+    textBubble: Optional[str]
+    textDark: Optional[str]
+    textGroupHeader: Optional[str]
+    textHeader: Optional[str]
+    textHeaderSelected: Optional[str]
+    textLight: Optional[str]
+    textMedium: Optional[str]
+
+class DataEditor(NoSSRComponent):
+    def get_event_triggers(self) -> Dict[str, Callable]: ...
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        rows: Optional[Union[Var[int], int]] = None,
+        columns: Optional[
+            Union[Var[List[Dict[str, Any]]], List[Dict[str, Any]]]
+        ] = None,
+        data: Optional[Union[Var[List[List[Any]]], List[List[Any]]]] = None,
+        get_cell_content: Optional[Union[Var[str], str]] = None,
+        get_cell_for_selection: Optional[Union[Var[bool], bool]] = None,
+        on_paste: Optional[Union[Var[bool], bool]] = None,
+        draw_focus_ring: Optional[Union[Var[bool], bool]] = None,
+        fixed_shadow_x: Optional[Union[Var[bool], bool]] = None,
+        fixed_shadow_y: Optional[Union[Var[bool], bool]] = None,
+        freeze_columns: Optional[Union[Var[int], int]] = None,
+        group_header_height: Optional[Union[Var[int], int]] = None,
+        header_height: Optional[Union[Var[int], int]] = None,
+        max_column_auto_width: Optional[Union[Var[int], int]] = None,
+        max_column_width: Optional[Union[Var[int], int]] = None,
+        min_column_width: Optional[Union[Var[int], int]] = None,
+        row_height: Optional[Union[Var[int], int]] = None,
+        row_markers: Optional[
+            Union[
+                Var[Literal["none", "number", "checkbox", "both", "clickable-number"]],
+                Literal["none", "number", "checkbox", "both", "clickable-number"],
+            ]
+        ] = None,
+        row_marker_start_index: Optional[Union[Var[int], int]] = None,
+        row_marker_width: Optional[Union[Var[int], int]] = None,
+        smooth_scroll_x: Optional[Union[Var[bool], bool]] = None,
+        smooth_scroll_y: Optional[Union[Var[bool], bool]] = None,
+        vertical_border: Optional[Union[Var[bool], bool]] = None,
+        column_select: Optional[Union[Var[str], str]] = None,
+        prevent_diagonal_scrolling: Optional[Union[Var[bool], bool]] = None,
+        overscroll_x: Optional[Union[Var[int], int]] = None,
+        overscroll_y: Optional[Union[Var[int], int]] = None,
+        scroll_offset_x: Optional[Union[Var[int], int]] = None,
+        scroll_offset_y: Optional[Union[Var[int], int]] = None,
+        theme: Optional[Union[Var[DataEditorTheme], DataEditorTheme]] = None,
+        style: Optional[Style] = None,
+        key: Optional[Any] = None,
+        id: Optional[Any] = None,
+        class_name: Optional[Any] = None,
+        autofocus: Optional[bool] = None,
+        custom_attrs: Optional[Dict[str, str]] = None,
+        on_cell_activated: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_cell_clicked: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_cell_context_menu: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_cell_edited: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_delete: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_finished_editing: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_group_header_clicked: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_group_header_context_menu: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_group_header_renamed: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_header_clicked: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_header_context_menu: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_header_menu_click: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_item_hovered: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_row_appended: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        on_selection_cleared: Optional[
+            Union[EventHandler, EventSpec, List, function, BaseVar]
+        ] = None,
+        **props
+    ) -> "DataEditor":
+        """Create the DataEditor component.
+
+        Args:
+            *children: The children of the data editor.
+            rows: Number of rows.
+            columns: Headers of the columns for the data grid.
+            data: The data.
+            get_cell_content: The name of the callback used to find the data to display.
+            get_cell_for_selection: Allow selection for copying.
+            on_paste: Allow paste.
+            draw_focus_ring: Controls the drawing of the focus ring.
+            fixed_shadow_x: Enables or disables the overlay shadow when scrolling horizontally.
+            fixed_shadow_y: Enables or disables the overlay shadow when scrolling vertically.
+            freeze_columns: The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
+            group_header_height: Controls the header of the group header row.
+            header_height: Controls the height of the header row.
+            max_column_auto_width: Additional header icons:  header_icons: Var[Any] # (TODO: must be a map of name: svg)  The maximum width a column can be automatically sized to.
+            max_column_width: The maximum width a column can be resized to.
+            min_column_width: The minimum width a column can be resized to.
+            row_height: Determins the height of each row.
+            row_markers: Kind of row markers.
+            row_marker_start_index: Changes the starting index for row markers.
+            row_marker_width: Sets the width of row markers in pixels, if unset row markers will automatically size.
+            smooth_scroll_x: Enable horizontal smooth scrolling.
+            smooth_scroll_y: Enable vertical smooth scrolling.
+            vertical_border: Controls the drawing of the left hand vertical border of a column. If set to a boolean value it controls all borders.
+            column_select: Allow columns selections. ("none", "single", "multiple")
+            prevent_diagonal_scrolling: Prevent diagonal scrolling.
+            overscroll_x: Allow to scroll past the limit of the actual content on the horizontal axis.
+            overscroll_y: Allow to scroll past the limit of the actual content on the vertical axis.
+            scroll_offset_x: Initial scroll offset on the horizontal axis.
+            scroll_offset_y: Initial scroll offset on the vertical axis.
+            theme: global theme
+            style: The style of the component.
+            key: A unique key for the component.
+            id: The id for the component.
+            class_name: The class name for the component.
+            autofocus: Whether the component should take the focus once the page is loaded
+            custom_attrs: custom attribute
+            **props: The props of the data editor.
+
+        Raises:
+            ValueError: invalid input.
+
+        Returns:
+            The DataEditor component.&
+        """
+        ...
+
+@serializer
+def serialize_dataeditortheme(theme: DataEditorTheme): ...

+ 5 - 0
reflex/components/literals.py

@@ -0,0 +1,5 @@
+"""Literal custom type used by Reflex."""
+
+from typing import Literal
+
+LiteralRowMarker = Literal["none", "number", "checkbox", "both", "clickable-number"]

+ 45 - 1
reflex/utils/format.py

@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Union
 from reflex import constants
 from reflex.utils import exceptions, serializers, types
 from reflex.utils.serializers import serialize
-from reflex.vars import Var
+from reflex.vars import BaseVar, Var
 
 if TYPE_CHECKING:
     from reflex.components.component import ComponentStyle
@@ -625,3 +625,47 @@ def unwrap_vars(value: str) -> str:
         string=value,
         flags=re.VERBOSE,
     )
+
+
+def format_data_editor_column(col: str | dict):
+    """Format a given column into the proper format.
+
+    Args:
+        col: The column.
+
+    Raises:
+        ValueError: invalid type provided for column.
+
+    Returns:
+        The formatted column.
+    """
+    if isinstance(col, str):
+        return {"title": col, "id": col.lower(), "type": "str"}
+
+    if isinstance(col, (dict,)):
+        if "id" not in col:
+            col["id"] = col["title"].lower()
+        if "type" not in col:
+            col["type"] = "str"
+        if "overlayIcon" not in col:
+            col["overlayIcon"] = None
+        return col
+
+    if isinstance(col, BaseVar):
+        return col
+
+    raise ValueError(
+        f"unexpected type ({(type(col).__name__)}: {col}) for column header in data_editor"
+    )
+
+
+def format_data_editor_cell(cell: Any):
+    """Format a given data into a renderable cell for data_editor.
+
+    Args:
+        cell: The data to format.
+
+    Returns:
+        The formatted cell.
+    """
+    return {"kind": Var.create(value="GridCellKind.Text"), "data": cell}

+ 13 - 0
reflex/utils/serializers.py

@@ -113,6 +113,19 @@ def has_serializer(type_: Type) -> bool:
     return get_serializer(type_) is not None
 
 
+@serializer
+def serialize_type(value: type) -> str:
+    """Serialize a python type.
+
+    Args:
+        value: the type to serialize.
+
+    Returns:
+        The serialized type.
+    """
+    return value.__name__
+
+
 @serializer
 def serialize_str(value: str) -> str:
     """Serialize a string.

+ 1 - 0
scripts/pyi_generator.py

@@ -30,6 +30,7 @@ EXCLUDED_FILES = [
     "foreach.py",
     "cond.py",
     "multiselect.py",
+    "literals.py",
 ]
 
 # These props exist on the base component, but should not be exposed in create methods.