Add datagrid editor (#1941)

This commit is contained in:
Thomas Brandého 2023-10-27 01:17:34 +02:00 committed by GitHub
parent 21dbdc0103
commit 9a5579e1ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 731 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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): ...

View File

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

View File

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

View File

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

View File

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