From 9a5579e1ef7dc0a7e9a7f98f4ce1c0c8619892b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Fri, 27 Oct 2023 01:17:34 +0200 Subject: [PATCH] Add datagrid editor (#1941) --- .../web/utils/helpers/dataeditor.js | 56 +++ reflex/components/__init__.py | 1 + reflex/components/datadisplay/__init__.py | 1 + reflex/components/datadisplay/dataeditor.py | 408 ++++++++++++++++++ reflex/components/datadisplay/dataeditor.pyi | 201 +++++++++ reflex/components/literals.py | 5 + reflex/utils/format.py | 46 +- reflex/utils/serializers.py | 13 + scripts/pyi_generator.py | 1 + 9 files changed, 731 insertions(+), 1 deletion(-) create mode 100644 reflex/.templates/web/utils/helpers/dataeditor.js create mode 100644 reflex/components/datadisplay/dataeditor.py create mode 100644 reflex/components/datadisplay/dataeditor.pyi create mode 100644 reflex/components/literals.py diff --git a/reflex/.templates/web/utils/helpers/dataeditor.js b/reflex/.templates/web/utils/helpers/dataeditor.js new file mode 100644 index 000000000..9d7fc0748 --- /dev/null +++ b/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" + } + }; +}; diff --git a/reflex/components/__init__.py b/reflex/components/__init__.py index ed12f0e61..9d01c92fe 100644 --- a/reflex/components/__init__.py +++ b/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 diff --git a/reflex/components/datadisplay/__init__.py b/reflex/components/datadisplay/__init__.py index 0de2cf40e..88bea861b 100644 --- a/reflex/components/datadisplay/__init__.py +++ b/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 diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py new file mode 100644 index 000000000..8936b12de --- /dev/null +++ b/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}) diff --git a/reflex/components/datadisplay/dataeditor.pyi b/reflex/components/datadisplay/dataeditor.pyi new file mode 100644 index 000000000..65be921e1 --- /dev/null +++ b/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): ... diff --git a/reflex/components/literals.py b/reflex/components/literals.py new file mode 100644 index 000000000..d46d1899a --- /dev/null +++ b/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"] diff --git a/reflex/utils/format.py b/reflex/utils/format.py index cf65a8672..df3c354df 100644 --- a/reflex/utils/format.py +++ b/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} diff --git a/reflex/utils/serializers.py b/reflex/utils/serializers.py index b8646d615..665aa10d3 100644 --- a/reflex/utils/serializers.py +++ b/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. diff --git a/scripts/pyi_generator.py b/scripts/pyi_generator.py index 2dcd5b20c..78ab0dbd5 100644 --- a/scripts/pyi_generator.py +++ b/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.