dataeditor.py 14 KB

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