dataeditor.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. """Data Editor component from glide-data-grid."""
  2. from __future__ import annotations
  3. from enum import Enum
  4. from typing import Any, Dict, Literal, Sequence, TypedDict
  5. from reflex.base import Base
  6. from reflex.components.component import Component, NoSSRComponent
  7. from reflex.components.literals import LiteralRowMarker
  8. from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
  9. from reflex.utils import console, format, types
  10. from reflex.utils.imports import ImportDict, ImportVar
  11. from reflex.utils.serializers import serializer
  12. from reflex.vars import get_unique_variable_name
  13. from reflex.vars.base import Var
  14. from reflex.vars.sequence import ArrayVar
  15. # TODO: Fix the serialization issue for custom types.
  16. class GridColumnIcons(Enum):
  17. """An Enum for the available icons in DataEditor."""
  18. Array = "array"
  19. AudioUri = "audio_uri"
  20. Boolean = "boolean"
  21. HeaderCode = "code"
  22. Date = "date"
  23. Email = "email"
  24. Emoji = "emoji"
  25. GeoDistance = "geo_distance"
  26. IfThenElse = "if_then_else"
  27. Image = "image"
  28. JoinStrings = "join_strings"
  29. Lookup = "lookup"
  30. Markdown = "markdown"
  31. Math = "math"
  32. Number = "number"
  33. Phone = "phone"
  34. Reference = "reference"
  35. Rollup = "rollup"
  36. RowID = "row_id"
  37. SingleValue = "single_value"
  38. SplitString = "split_string"
  39. String = "string"
  40. TextTemplate = "text_template"
  41. Time = "time"
  42. Uri = "uri"
  43. VideoUri = "video_uri"
  44. class DataEditorTheme(Base):
  45. """The theme for the DataEditor component."""
  46. accent_color: str | None = None
  47. accent_fg: str | None = None
  48. accent_light: str | None = None
  49. base_font_style: str | None = None
  50. bg_bubble: str | None = None
  51. bg_bubble_selected: str | None = None
  52. bg_cell: str | None = None
  53. bg_cell_medium: str | None = None
  54. bg_header: str | None = None
  55. bg_header_has_focus: str | None = None
  56. bg_header_hovered: str | None = None
  57. bg_icon_header: str | None = None
  58. bg_search_result: str | None = None
  59. border_color: str | None = None
  60. cell_horizontal_padding: int | None = None
  61. cell_vertical_padding: int | None = None
  62. drilldown_border: str | None = None
  63. editor_font_size: str | None = None
  64. fg_icon_header: str | None = None
  65. font_family: str | None = None
  66. header_bottom_border_color: str | None = None
  67. header_font_style: str | None = None
  68. horizontal_border_color: str | None = None
  69. line_height: int | None = None
  70. link_color: str | None = None
  71. text_bubble: str | None = None
  72. text_dark: str | None = None
  73. text_group_header: str | None = None
  74. text_header: str | None = None
  75. text_header_selected: str | None = None
  76. text_light: str | None = None
  77. text_medium: str | None = None
  78. class Bounds(TypedDict):
  79. """The bounds of the group header."""
  80. x: int
  81. y: int
  82. width: int
  83. height: int
  84. class CompatSelection(TypedDict):
  85. """The selection."""
  86. items: list
  87. class Rectangle(TypedDict):
  88. """The bounds of the group header."""
  89. x: int
  90. y: int
  91. width: int
  92. height: int
  93. class GridSelectionCurrent(TypedDict):
  94. """The current selection."""
  95. cell: tuple[int, int]
  96. range: Rectangle
  97. rangeStack: list[Rectangle]
  98. class GridSelection(TypedDict):
  99. """The grid selection."""
  100. current: GridSelectionCurrent | None
  101. columns: CompatSelection
  102. rows: CompatSelection
  103. class GroupHeaderClickedEventArgs(TypedDict):
  104. """The arguments for the group header clicked event."""
  105. kind: str
  106. group: str
  107. location: tuple[int, int]
  108. bounds: Bounds
  109. isEdge: bool
  110. shiftKey: bool
  111. ctrlKey: bool
  112. metaKey: bool
  113. isTouch: bool
  114. localEventX: int
  115. localEventY: int
  116. button: int
  117. buttons: int
  118. scrollEdge: tuple[int, int]
  119. class GridCell(TypedDict):
  120. """The grid cell."""
  121. span: list[int] | None
  122. class GridColumn(TypedDict):
  123. """The grid column."""
  124. title: str
  125. group: str | None
  126. class DataEditor(NoSSRComponent):
  127. """The DataEditor Component."""
  128. tag = "DataEditor"
  129. is_default = True
  130. library: str | None = "@glideapps/glide-data-grid@^6.0.3"
  131. lib_dependencies: list[str] = [
  132. "lodash@^4.17.21",
  133. "react-responsive-carousel@^3.2.7",
  134. ]
  135. # Number of rows.
  136. rows: Var[int]
  137. # Headers of the columns for the data grid.
  138. columns: Var[Sequence[dict[str, Any]]]
  139. # The data.
  140. data: Var[Sequence[Sequence[Any]]]
  141. # The name of the callback used to find the data to display.
  142. get_cell_content: Var[str]
  143. # Allow selection for copying.
  144. get_cells_for_selection: Var[bool]
  145. # Allow paste.
  146. on_paste: Var[bool]
  147. # Controls the drawing of the focus ring.
  148. draw_focus_ring: Var[bool]
  149. # Enables or disables the overlay shadow when scrolling horizontally.
  150. fixed_shadow_x: Var[bool]
  151. # Enables or disables the overlay shadow when scrolling vertically.
  152. fixed_shadow_y: Var[bool]
  153. # The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
  154. freeze_columns: Var[int]
  155. # Controls the header of the group header row.
  156. group_header_height: Var[int]
  157. # Controls the height of the header row.
  158. header_height: Var[int]
  159. # Additional header icons:
  160. # header_icons: Var[Any] # (TODO: must be a map of name: svg) #noqa: ERA001
  161. # The maximum width a column can be automatically sized to.
  162. max_column_auto_width: Var[int]
  163. # The maximum width a column can be resized to.
  164. max_column_width: Var[int]
  165. # The minimum width a column can be resized to.
  166. min_column_width: Var[int]
  167. # Determines the height of each row.
  168. row_height: Var[int]
  169. # Kind of row markers.
  170. row_markers: Var[LiteralRowMarker]
  171. # Changes the starting index for row markers.
  172. row_marker_start_index: Var[int]
  173. # Sets the width of row markers in pixels, if unset row markers will automatically size.
  174. row_marker_width: Var[int]
  175. # Enable horizontal smooth scrolling.
  176. smooth_scroll_x: Var[bool]
  177. # Enable vertical smooth scrolling.
  178. smooth_scroll_y: Var[bool]
  179. # Controls the drawing of the left hand vertical border of a column. If set to a boolean value it controls all borders.
  180. vertical_border: Var[bool] # TODO: support a mapping (dict[int, bool])
  181. # Allow columns selections. ("none", "single", "multi")
  182. column_select: Var[Literal["none", "single", "multi"]]
  183. # Prevent diagonal scrolling.
  184. prevent_diagonal_scrolling: Var[bool]
  185. # Allow to scroll past the limit of the actual content on the horizontal axis.
  186. overscroll_x: Var[int]
  187. # Allow to scroll past the limit of the actual content on the vertical axis.
  188. overscroll_y: Var[int]
  189. # Initial scroll offset on the horizontal axis.
  190. scroll_offset_x: Var[int]
  191. # Initial scroll offset on the vertical axis.
  192. scroll_offset_y: Var[int]
  193. # global theme
  194. theme: Var[DataEditorTheme | Dict]
  195. # Fired when a cell is activated.
  196. on_cell_activated: EventHandler[passthrough_event_spec(tuple[int, int])]
  197. # Fired when a cell is clicked.
  198. on_cell_clicked: EventHandler[passthrough_event_spec(tuple[int, int])]
  199. # Fired when a cell is right-clicked.
  200. on_cell_context_menu: EventHandler[passthrough_event_spec(tuple[int, int])]
  201. # Fired when a cell is edited.
  202. on_cell_edited: EventHandler[passthrough_event_spec(tuple[int, int], GridCell)]
  203. # Fired when a group header is clicked.
  204. on_group_header_clicked: EventHandler[
  205. passthrough_event_spec(tuple[int, int], GridCell)
  206. ]
  207. # Fired when a group header is right-clicked.
  208. on_group_header_context_menu: EventHandler[
  209. passthrough_event_spec(int, GroupHeaderClickedEventArgs)
  210. ]
  211. # Fired when a group header is renamed.
  212. on_group_header_renamed: EventHandler[passthrough_event_spec(str, str)]
  213. # Fired when a header is clicked.
  214. on_header_clicked: EventHandler[passthrough_event_spec(tuple[int, int])]
  215. # Fired when a header is right-clicked.
  216. on_header_context_menu: EventHandler[passthrough_event_spec(tuple[int, int])]
  217. # Fired when a header menu item is clicked.
  218. on_header_menu_click: EventHandler[passthrough_event_spec(int, Rectangle)]
  219. # Fired when an item is hovered.
  220. on_item_hovered: EventHandler[passthrough_event_spec(tuple[int, int])]
  221. # Fired when a selection is deleted.
  222. on_delete: EventHandler[passthrough_event_spec(GridSelection)]
  223. # Fired when editing is finished.
  224. on_finished_editing: EventHandler[
  225. passthrough_event_spec(GridCell | None, tuple[int, int])
  226. ]
  227. # Fired when a row is appended.
  228. on_row_appended: EventHandler[no_args_event_spec]
  229. # Fired when the selection is cleared.
  230. on_selection_cleared: EventHandler[no_args_event_spec]
  231. # Fired when a column is resized.
  232. on_column_resize: EventHandler[passthrough_event_spec(GridColumn, int)]
  233. def add_imports(self) -> ImportDict:
  234. """Add imports for the component.
  235. Returns:
  236. The import dict.
  237. """
  238. if self.library is None:
  239. return {}
  240. return {
  241. "": f"{format.format_library_name(self.library)}/dist/index.css",
  242. self.library: "GridCellKind",
  243. "$/utils/helpers/dataeditor.js": ImportVar(
  244. tag="formatDataEditorCells", is_default=False, install=False
  245. ),
  246. }
  247. def add_hooks(self) -> list[str]:
  248. """Get the hooks to render.
  249. Returns:
  250. The hooks to render.
  251. """
  252. # Define the id of the component in case multiple are used in the same page.
  253. editor_id = get_unique_variable_name()
  254. # Define the name of the getData callback associated with this component and assign to get_cell_content.
  255. if self.get_cell_content is not None:
  256. data_callback = self.get_cell_content._js_expr
  257. else:
  258. data_callback = f"getData_{editor_id}"
  259. self.get_cell_content = Var(_js_expr=data_callback)
  260. code = [f"function {data_callback}([col, row]){{"]
  261. columns_path = str(self.columns)
  262. data_path = str(self.data)
  263. code.extend(
  264. [
  265. f" return formatDataEditorCells(col, row, {columns_path}, {data_path});",
  266. " }",
  267. ]
  268. )
  269. return ["\n".join(code)]
  270. @classmethod
  271. def create(cls, *children, **props) -> Component:
  272. """Create the DataEditor component.
  273. Args:
  274. *children: The children of the data editor.
  275. **props: The props of the data editor.
  276. Raises:
  277. ValueError: invalid input.
  278. Returns:
  279. The DataEditor component.&
  280. """
  281. from reflex.components.el import Div
  282. columns = props.get("columns", [])
  283. data = props.get("data", [])
  284. rows = props.get("rows")
  285. # If rows is not provided, determine from data.
  286. if rows is None:
  287. if isinstance(data, Var) and not isinstance(data, ArrayVar):
  288. raise ValueError(
  289. "DataEditor data must be an ArrayVar if rows is not provided."
  290. )
  291. props["rows"] = data.length() if isinstance(data, ArrayVar) else len(data)
  292. if not isinstance(columns, Var) and len(columns):
  293. if types.is_dataframe(type(data)) or (
  294. isinstance(data, Var) and types.is_dataframe(data._var_type)
  295. ):
  296. raise ValueError(
  297. "Cannot pass in both a pandas dataframe and columns to the data_editor component."
  298. )
  299. else:
  300. props["columns"] = [
  301. format.format_data_editor_column(col) for col in columns
  302. ]
  303. if "theme" in props:
  304. theme = props.get("theme")
  305. if isinstance(theme, Dict):
  306. props["theme"] = DataEditorTheme(**theme)
  307. # Allow by default to select a region of cells in the grid.
  308. props.setdefault("get_cells_for_selection", True)
  309. # Disable on_paste by default if not provided.
  310. props.setdefault("on_paste", False)
  311. if props.pop("get_cell_content", None) is not None:
  312. console.warn(
  313. "get_cell_content is not user configurable, the provided value will be discarded"
  314. )
  315. grid = super().create(*children, **props)
  316. return Div.create(
  317. grid,
  318. width=props.pop("width", "100%"),
  319. height=props.pop("height", "100%"),
  320. )
  321. @staticmethod
  322. def _get_app_wrap_components() -> dict[tuple[int, str], Component]:
  323. """Get the app wrap components for the component.
  324. Returns:
  325. The app wrap components.
  326. """
  327. from reflex.components.el import Div
  328. class Portal(Div):
  329. def get_ref(self):
  330. return None
  331. return {
  332. (-1, "DataEditorPortal"): Portal.create(
  333. id="portal",
  334. position="fixed",
  335. top=0,
  336. )
  337. }
  338. @serializer
  339. def serialize_dataeditortheme(theme: DataEditorTheme):
  340. """The serializer for the data editor theme.
  341. Args:
  342. theme: The theme to serialize.
  343. Returns:
  344. The serialized theme.
  345. """
  346. return {
  347. format.to_camel_case(k): v for k, v in theme.__dict__.items() if v is not None
  348. }
  349. data_editor = DataEditor.create
  350. data_editor_theme = DataEditorTheme