From c3a7294b43b11a7692a310f4a96fe90321716d0b Mon Sep 17 00:00:00 2001 From: zain2983 Date: Mon, 2 Feb 2026 02:59:31 +0500 Subject: [PATCH 1/2] fix(data_table): resolve blank cells for non-camelCase column keys (#6108) --- pyi_hashes.json | 121 +------------------------- reflex/components/gridjs/datatable.py | 9 ++ 2 files changed, 10 insertions(+), 120 deletions(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 185f64eccae..271e7d1b10a 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -1,122 +1,3 @@ { - "reflex/__init__.pyi": "0a3ae880e256b9fd3b960e12a2cb51a7", - "reflex/components/__init__.pyi": "ac05995852baa81062ba3d18fbc489fb", - "reflex/components/base/__init__.pyi": "16e47bf19e0d62835a605baa3d039c5a", - "reflex/components/base/app_wrap.pyi": "22e94feaa9fe675bcae51c412f5b67f1", - "reflex/components/base/body.pyi": "e8ab029a730824bab6d4211203609e6a", - "reflex/components/base/document.pyi": "311c53c90a60587a82e760103758a3cf", - "reflex/components/base/error_boundary.pyi": "a678cceea014cb16048647257cd24ba6", - "reflex/components/base/fragment.pyi": "745f1be02c23a0b25d7c52d7423ec76a", - "reflex/components/base/link.pyi": "0bc1d26ee29d8864aed14a12991bd47d", - "reflex/components/base/meta.pyi": "129aecf65ab53f756c4d1cbe1d0b188d", - "reflex/components/base/script.pyi": "e5f506d1d0d6712cb9e597a781eb3941", - "reflex/components/base/strict_mode.pyi": "6b72e16caadf7158ab744a0ab751b010", - "reflex/components/core/__init__.pyi": "007170b97e58bdf28b2aee381d91c0c7", - "reflex/components/core/auto_scroll.pyi": "18068d22aca7244a08cd0c5a897c0950", - "reflex/components/core/banner.pyi": "fd93e7a92961de8524718ad32135c37c", - "reflex/components/core/clipboard.pyi": "a844eb927d9bc2a43f5e88161b258539", - "reflex/components/core/debounce.pyi": "055da7aa890f44fb4d48bd5978f1a874", - "reflex/components/core/helmet.pyi": "43f8497c8fafe51e29dca1dd535d143a", - "reflex/components/core/html.pyi": "86eb9d4c1bb4807547b2950d9a32e9fd", - "reflex/components/core/sticky.pyi": "cb763b986a9b0654d1a3f33440dfcf60", - "reflex/components/core/upload.pyi": "6dc28804a6dddf903e31162e87c1b023", - "reflex/components/core/window_events.pyi": "af33ccec866b9540ee7fbec6dbfbd151", - "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", - "reflex/components/datadisplay/code.pyi": "b86769987ef4d1cbdddb461be88539fd", - "reflex/components/datadisplay/dataeditor.pyi": "fb26f3e702fcb885539d1cf82a854be3", - "reflex/components/datadisplay/shiki_code_block.pyi": "1d53e75b6be0d3385a342e7b3011babd", - "reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc", - "reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4", - "reflex/components/el/elements/__init__.pyi": "29512d7a6b29c6dc5ff68d3b31f26528", - "reflex/components/el/elements/base.pyi": "3f74c7ea573ea29b055b0cd48b040d2c", - "reflex/components/el/elements/forms.pyi": "8b6bb2fbaf4bad828b076e2f7c8444d0", - "reflex/components/el/elements/inline.pyi": "3549cd6ad45217aa6387800911b641c3", - "reflex/components/el/elements/media.pyi": "9b97220aa99783d402b6e278c4069043", - "reflex/components/el/elements/metadata.pyi": "24448004b7aa07f1225028a85bd49fef", - "reflex/components/el/elements/other.pyi": "0c4d5d0b955d8596bf6cf4a48d7decdb", - "reflex/components/el/elements/scripts.pyi": "d33df9f21f7e838376b2b5024beef7c9", - "reflex/components/el/elements/sectioning.pyi": "3c5a7e4caa9c25da0ae788f02466eac4", - "reflex/components/el/elements/tables.pyi": "686eb70ea7d8c4dafb0cc5c284e76184", - "reflex/components/el/elements/typography.pyi": "684e83dde887dba12badd0fb75c87c04", - "reflex/components/gridjs/datatable.pyi": "98a7e1b3f3b60cafcdfcd8879750ee42", - "reflex/components/lucide/icon.pyi": "9cdd1107295f5c4b6d5d6516f487f237", - "reflex/components/markdown/markdown.pyi": "dd74e8e9665b2a813ff799a7aa190b44", - "reflex/components/moment/moment.pyi": "e1952f1c2c82cef85d91e970d1be64ab", - "reflex/components/plotly/plotly.pyi": "4311a0aae2abcc9226abb6a273f96372", - "reflex/components/radix/__init__.pyi": "5d8e3579912473e563676bfc71f29191", - "reflex/components/radix/primitives/__init__.pyi": "01c388fe7a1f5426a16676404344edf6", - "reflex/components/radix/primitives/accordion.pyi": "19484eca0ad53f538f5db04c09921738", - "reflex/components/radix/primitives/base.pyi": "9ef34884fb6028dc017df5e2db639c81", - "reflex/components/radix/primitives/dialog.pyi": "9ee73362bb59619c482b6b0d07033f37", - "reflex/components/radix/primitives/drawer.pyi": "921e45dfaf5b9131ef27c561c3acca2e", - "reflex/components/radix/primitives/form.pyi": "78055e820703c98c3b838aa889566365", - "reflex/components/radix/primitives/progress.pyi": "c917952d57ddb3e138a40c4005120d5e", - "reflex/components/radix/primitives/slider.pyi": "4ff06f0025d47f166132909b09ab96f8", - "reflex/components/radix/themes/__init__.pyi": "582b4a7ead62b2ae8605e17fa084c063", - "reflex/components/radix/themes/base.pyi": "3e1ccd5ce5fef0b2898025193ee3d069", - "reflex/components/radix/themes/color_mode.pyi": "dda570583355d8c0d8f607be457ba7a1", - "reflex/components/radix/themes/components/__init__.pyi": "efa279ee05479d7bb8a64d49da808d03", - "reflex/components/radix/themes/components/alert_dialog.pyi": "eed422fcc1ff5ccf3dbf6934699bd0b1", - "reflex/components/radix/themes/components/aspect_ratio.pyi": "71de4160d79840561c48b570197a4152", - "reflex/components/radix/themes/components/avatar.pyi": "e40c2f0fda6d2c028d83681a27f3fb96", - "reflex/components/radix/themes/components/badge.pyi": "58fd1a9c5d2f8762e2a0370311731ff5", - "reflex/components/radix/themes/components/button.pyi": "50f0b08ad5d1d1054ab537152f0f5c43", - "reflex/components/radix/themes/components/callout.pyi": "547f2570ffbd10db36b745566e9f1b17", - "reflex/components/radix/themes/components/card.pyi": "f7adb83f7b001a11bdd7fd6791fb3ffb", - "reflex/components/radix/themes/components/checkbox.pyi": "8eabb6887a5d0849a43e086a284814c2", - "reflex/components/radix/themes/components/checkbox_cards.pyi": "1d567fd04b4425abd5cc5aad10108aa9", - "reflex/components/radix/themes/components/checkbox_group.pyi": "8638582a623036f8893a3fa6080f2672", - "reflex/components/radix/themes/components/context_menu.pyi": "b9499d8bdd2c5565621fea5fe7d7a25a", - "reflex/components/radix/themes/components/data_list.pyi": "6f8d9c582e084c23966b992158193b72", - "reflex/components/radix/themes/components/dialog.pyi": "d2615f1a68c80ff930444d054b598c13", - "reflex/components/radix/themes/components/dropdown_menu.pyi": "43f8770c9adf93c73398d68f79048424", - "reflex/components/radix/themes/components/hover_card.pyi": "a96f4433237f9994decf935deff9f269", - "reflex/components/radix/themes/components/icon_button.pyi": "e930911d8ecbe61e5447e61c76a28ab6", - "reflex/components/radix/themes/components/inset.pyi": "bd7a2186b553bd4c86d83ff50c784066", - "reflex/components/radix/themes/components/popover.pyi": "91f8edefeb232cc6d48690b1838144c2", - "reflex/components/radix/themes/components/progress.pyi": "0e59587d5b3c8fe0d0067587f144e5b0", - "reflex/components/radix/themes/components/radio.pyi": "f375aa5ac746679618ea7dad257e3224", - "reflex/components/radix/themes/components/radio_cards.pyi": "9dc34a1ce2a1924eb1f41438ef84e80b", - "reflex/components/radix/themes/components/radio_group.pyi": "173254cf91908bcf6aa4fa21a747e2cf", - "reflex/components/radix/themes/components/scroll_area.pyi": "2e3539b0f6895dda127ee96e9864dbf9", - "reflex/components/radix/themes/components/segmented_control.pyi": "1776f1ad936bae402007802b1ee98906", - "reflex/components/radix/themes/components/select.pyi": "2c7aee592972ff5f05da08154aa981c8", - "reflex/components/radix/themes/components/separator.pyi": "79e550cc10ee455f35d75d0e236fedd2", - "reflex/components/radix/themes/components/skeleton.pyi": "a25d3ceb56f99f736ea463579845c454", - "reflex/components/radix/themes/components/slider.pyi": "305a34c14ca8656ca9267e4c31aaa388", - "reflex/components/radix/themes/components/spinner.pyi": "b7e689e7d75635e379242fd113a1ea9a", - "reflex/components/radix/themes/components/switch.pyi": "f1ba948750a74126cda990e89a3ec7ef", - "reflex/components/radix/themes/components/table.pyi": "eefbbd1904deae3d166fcad28b20fd4a", - "reflex/components/radix/themes/components/tabs.pyi": "a533d2509a6798fe0ab7275b0152519d", - "reflex/components/radix/themes/components/text_area.pyi": "4af55e5d18a5b9d56717bf31b23ea543", - "reflex/components/radix/themes/components/text_field.pyi": "232618b744076db98d861ea1b9eb3192", - "reflex/components/radix/themes/components/tooltip.pyi": "2b8366200ce92ec4784ca3ec4152e676", - "reflex/components/radix/themes/layout/__init__.pyi": "73eefc509a49215b1797b5b5d28d035e", - "reflex/components/radix/themes/layout/base.pyi": "5be31d7dadd23ab544e53762423d123e", - "reflex/components/radix/themes/layout/box.pyi": "dbaed1c50c668805fc7b71d22f878254", - "reflex/components/radix/themes/layout/center.pyi": "17323694217e8ad7611adb683f8d96ce", - "reflex/components/radix/themes/layout/container.pyi": "24222fd7ffa2dc05f709eab6c7b9643c", - "reflex/components/radix/themes/layout/flex.pyi": "0307e9dbe6a5784140121d77c8f67a86", - "reflex/components/radix/themes/layout/grid.pyi": "95c9edb8bdd4e39dc1bd6bc2a8ca0933", - "reflex/components/radix/themes/layout/list.pyi": "049ecf827ef0ba8de2d76dbf7b1c562c", - "reflex/components/radix/themes/layout/section.pyi": "a51952b9b5c8227aa3024373dedcad5d", - "reflex/components/radix/themes/layout/spacer.pyi": "c35accf0f2f742c90a23675ff1fb960d", - "reflex/components/radix/themes/layout/stack.pyi": "271d3315c6196356d3ced759520d4e7d", - "reflex/components/radix/themes/typography/__init__.pyi": "b8ef970530397e9984004961f3aaee62", - "reflex/components/radix/themes/typography/blockquote.pyi": "080c71899532f5dbf4cf143e7a5ad3bf", - "reflex/components/radix/themes/typography/code.pyi": "7ffe785d55979cf8ff97ea040f3e2b64", - "reflex/components/radix/themes/typography/heading.pyi": "0ebb38915cd0521fd59c569e04d288bb", - "reflex/components/radix/themes/typography/link.pyi": "e88c5d880a54548b6808c097ac62505b", - "reflex/components/radix/themes/typography/text.pyi": "50f9ca15a941e4b77ddd12e77aa3c03e", - "reflex/components/react_player/audio.pyi": "0e1690ff1f1f39bc748278d292238350", - "reflex/components/react_player/react_player.pyi": "5ccd373b94ed1d3934ae6afc46bd6fe4", - "reflex/components/react_player/video.pyi": "998671c06103d797c554d9278eb3b2a0", - "reflex/components/react_router/dom.pyi": "3042fa630b7e26a7378fe045d7fbf4af", - "reflex/components/recharts/__init__.pyi": "6ee7f1ca2c0912f389ba6f3251a74d99", - "reflex/components/recharts/cartesian.pyi": "cfca4f880239ffaecdf9fb4c7c8caed5", - "reflex/components/recharts/charts.pyi": "013036b9c00ad85a570efdb813c1bc40", - "reflex/components/recharts/general.pyi": "d87ff9b85b2a204be01753690df4fb11", - "reflex/components/recharts/polar.pyi": "ad24bd37c6acc0bc9bd4ac01af3ffe49", - "reflex/components/recharts/recharts.pyi": "c41d19ab67972246c574098929bea7ea", - "reflex/components/sonner/toast.pyi": "3c27bad1aaeb5183eaa6a41e77e8d7f0" + "reflex/components/gridjs/datatable.pyi": "98a7e1b3f3b60cafcdfcd8879750ee42" } diff --git a/reflex/components/gridjs/datatable.py b/reflex/components/gridjs/datatable.py index 86a087f555f..da1966761c9 100644 --- a/reflex/components/gridjs/datatable.py +++ b/reflex/components/gridjs/datatable.py @@ -127,5 +127,14 @@ def _render(self) -> Tag: self.columns = LiteralVar.create(data["columns"]) self.data = LiteralVar.create(data["data"]) + # If columns is a list of strings convert to list of dicts with id and name keys + if isinstance(self.columns, LiteralVar) and isinstance( + self.columns._var_value, list + ): + self.columns = LiteralVar.create([ + {"id": col, "name": col} if isinstance(col, str) else col + for col in self.columns._var_value + ]) + # Render the table. return super()._render() From afc916aece11e1384f7ab344a521dab7a87c74c4 Mon Sep 17 00:00:00 2001 From: zain2983 Date: Tue, 10 Feb 2026 03:10:27 +0500 Subject: [PATCH 2/2] fix(datatable): ensure columns work with DataFrame, list, and Var inputs --- pyi_hashes.json | 2 +- reflex/components/gridjs/datatable.py | 146 +++++++++++------- tests/units/components/gridjs/__init__.py | 1 + .../units/components/gridjs/test_datatable.py | 90 +++++++++++ 4 files changed, 183 insertions(+), 56 deletions(-) create mode 100644 tests/units/components/gridjs/__init__.py create mode 100644 tests/units/components/gridjs/test_datatable.py diff --git a/pyi_hashes.json b/pyi_hashes.json index 271e7d1b10a..417170c20b5 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -1,3 +1,3 @@ { - "reflex/components/gridjs/datatable.pyi": "98a7e1b3f3b60cafcdfcd8879750ee42" + "reflex/components/gridjs/datatable.pyi": "e1f34ade3873a931770da4a35586f298" } diff --git a/reflex/components/gridjs/datatable.py b/reflex/components/gridjs/datatable.py index da1966761c9..59cfe9499a9 100644 --- a/reflex/components/gridjs/datatable.py +++ b/reflex/components/gridjs/datatable.py @@ -1,4 +1,4 @@ -"""Table components.""" +"""Table components for Reflex using Gridjs.""" from __future__ import annotations @@ -14,58 +14,52 @@ class Gridjs(NoSSRComponent): - """A component that wraps a nivo bar component.""" + """A base component that wraps Gridjs (JS library) for tables.""" library = "gridjs-react@6.1.1" - lib_dependencies: list[str] = ["gridjs@6.2.0"] class DataTable(Gridjs): - """A data table component.""" + """A flexible data table component for Reflex. - tag = "Grid" + Supports: + - Pandas DataFrames + - Python lists + - Reflex Vars (state variables) + """ + tag = "Grid" alias = "DataTableGrid" - # The data to display. Either a list of lists or a pandas dataframe. - data: Any - - # The list of columns to display. Required if data is a list and should not be provided - # if the data field is a dataframe - columns: Var[Sequence] - - # Enable a search bar. - search: Var[bool] - - # Enable sorting on columns. - sort: Var[bool] - - # Enable resizable columns. - resizable: Var[bool] - - # Enable pagination. - pagination: Var[bool | dict] - + # ----------------------------- + # Component Props + # ----------------------------- + data: Any # The data to display (list of lists or DataFrame) + columns: Var[Sequence] # Columns to display (optional if using DataFrame) + search: Var[bool] # Enable search + sort: Var[bool] # Enable sorting + resizable: Var[bool] # Enable column resizing + pagination: Var[bool | dict] # Enable pagination + + # ----------------------------- + # Component creation + # ----------------------------- @classmethod def create(cls, *children, **props): - """Create a datatable component. + """Create a DataTable component with proper validation. - Args: - *children: The children of the component. - **props: The props to pass to the component. + Raises: + ValueError: If both DataFrame and columns are provided, or + if columns are missing for a list-type data field. Returns: - The datatable component. - - Raises: - ValueError: If a pandas dataframe is passed in and columns are also provided. + DataTable: The created DataTable component. """ data = props.get("data") columns = props.get("columns") - # The annotation should be provided if data is a computed var. We need this to know how to - # render pandas dataframes. + # 1️⃣ Ensure computed Vars have type annotations if is_computed_var(data) and data._var_type == Any: msg = "Annotation of the computed var assigned to the data field should be provided." raise ValueError(msg) @@ -78,7 +72,7 @@ def create(cls, *children, **props): msg = "Annotation of the computed var assigned to the column field should be provided." raise ValueError(msg) - # If data is a pandas dataframe and columns are provided throw an error. + # 2️⃣ Disallow DataFrame + columns (columns auto-detected from DataFrame) if ( types.is_dataframe(type(data)) or (isinstance(data, Var) and types.is_dataframe(data._var_type)) @@ -86,30 +80,42 @@ def create(cls, *children, **props): msg = "Cannot pass in both a pandas dataframe and columns to the data_table component." raise ValueError(msg) - # If data is a list and columns are not provided, throw an error + # 3️⃣ Require columns if data is a list if ( (isinstance(data, Var) and types.typehint_issubclass(data._var_type, list)) or isinstance(data, list) ) and columns is None: - msg = "column field should be specified when the data field is a list type" + msg = "Column field should be specified when the data field is a list type" raise ValueError(msg) - # Create the component. - return super().create( - *children, - **props, - ) + # 4️⃣ Call parent create method + return super().create(*children, **props) + # ----------------------------- + # Add external imports (CSS) + # ----------------------------- def add_imports(self) -> ImportDict: - """Add the imports for the datatable component. + """Add CSS for Gridjs. Returns: - The import dict for the component. + ImportDict: The import dictionary required for the component. """ return {"": "gridjs/dist/theme/mermaid.css"} + # ----------------------------- + # Render component + # ----------------------------- def _render(self) -> Tag: + """Normalize columns and prepare data for front-end rendering. + + Returns: + Tag: The rendered table component. + """ + # ----------------------------- + # Case 1: DataFrame coming from State (Var) + # ----------------------------- if isinstance(self.data, Var) and types.is_dataframe(self.data._var_type): + # Convert DataFrame to front-end-safe Vars self.columns = self.data._replace( _js_expr=f"{self.data._js_expr}.columns", _var_type=list[Any], @@ -118,23 +124,53 @@ def _render(self) -> Tag: _js_expr=f"{self.data._js_expr}.data", _var_type=list[list[Any]], ) + + # ----------------------------- + # Case 2: DataFrame passed directly from Python + # ----------------------------- if types.is_dataframe(type(self.data)): - # If given a pandas df break up the data and columns data = serialize(self.data) if not isinstance(data, dict): msg = "Serialized dataframe should be a dict." raise ValueError(msg) + + # Convert Python lists to LiteralVars for front-end rendering self.columns = LiteralVar.create(data["columns"]) self.data = LiteralVar.create(data["data"]) - # If columns is a list of strings convert to list of dicts with id and name keys - if isinstance(self.columns, LiteralVar) and isinstance( - self.columns._var_value, list - ): - self.columns = LiteralVar.create([ - {"id": col, "name": col} if isinstance(col, str) else col - for col in self.columns._var_value - ]) - - # Render the table. + # ----------------------------- + # Case 3: Normalize columns for all other scenarios + # ----------------------------- + if self.columns is not None: + # Python list → LiteralVar + if isinstance(self.columns, list): + self.columns = LiteralVar.create([ + {"id": col, "name": col} if isinstance(col, str) else col + for col in self.columns + ]) + + # LiteralVar[list] → normalized LiteralVar + elif isinstance(self.columns, LiteralVar) and isinstance( + self.columns._var_value, list + ): + self.columns = LiteralVar.create([ + {"id": col, "name": col} if isinstance(col, str) else col + for col in self.columns._var_value + ]) + + # Var[list] → frontend-safe JS mapping (compile-time + runtime safe) + elif isinstance(self.columns, Var): + self.columns = self.columns._replace( + _js_expr=( + f"{self.columns._js_expr}.map(" + "(col) => typeof col === 'string' " + "? ({ id: col, name: col }) " + ": col)" + ), + _var_type=list[Any], + ) + + # ----------------------------- + # Case 4: Render component + # ----------------------------- return super()._render() diff --git a/tests/units/components/gridjs/__init__.py b/tests/units/components/gridjs/__init__.py new file mode 100644 index 00000000000..b271b8cd325 --- /dev/null +++ b/tests/units/components/gridjs/__init__.py @@ -0,0 +1 @@ +"""gridjs component tests.""" diff --git a/tests/units/components/gridjs/test_datatable.py b/tests/units/components/gridjs/test_datatable.py new file mode 100644 index 00000000000..4d292cd8762 --- /dev/null +++ b/tests/units/components/gridjs/test_datatable.py @@ -0,0 +1,90 @@ +import pandas as pd +import pytest + +from reflex.components.gridjs import DataTable +from reflex.state import State +from reflex.vars.base import LiteralVar, Var + + +class TableState(State): + """TableState used by tests.""" + + df: pd.DataFrame = pd.DataFrame({"A": [1], "B": [2]}) + data: list[list[int]] = [[1, 2]] + columns: list[str] = ["A", "B"] + + +def test_dataframe_python_columns_normalized(): + """DataFrame passed as a Python value should produce normalized LiteralVar columns.""" + df = pd.DataFrame({"A": [1], "B": [2]}) + + table = DataTable.create(data=df) + table._render() + + columns = table.columns # type: ignore[attr-defined] + + assert isinstance(columns, LiteralVar) + assert columns._var_value == [ + {"id": "A", "name": "A"}, + {"id": "B", "name": "B"}, + ] + + +def test_dataframe_var_columns_preserved(): + """DataFrame coming from State is a runtime value. + Columns must remain a Var (frontend expression), + not be eagerly converted to LiteralVar. + """ + table = DataTable.create(data=TableState.df) + table._render() + + columns = table.columns # type: ignore[attr-defined] + + assert isinstance(columns, Var) + assert not isinstance(columns, LiteralVar) + + +def test_list_columns_python_normalized(): + """Python list of column names should be normalized eagerly.""" + table = DataTable.create( + data=[[1, 2]], + columns=["A", "B"], + ) + table._render() + + columns = table.columns # type: ignore[attr-defined] + + assert isinstance(columns, LiteralVar) + assert columns._var_value == [ + {"id": "A", "name": "A"}, + {"id": "B", "name": "B"}, + ] + + +def test_list_columns_var_preserved(): + """Columns coming from State must remain a Var so they + can be transformed on the frontend. + """ + table = DataTable.create( + data=TableState.data, + columns=TableState.columns, + ) + table._render() + + columns = table.columns # type: ignore[attr-defined] + + assert isinstance(columns, Var) + assert not isinstance(columns, LiteralVar) + + +def test_dataframe_with_columns_raises(): + """DataFrame already defines columns. + Passing explicit columns is ambiguous and must error. + """ + df = pd.DataFrame({"A": [1]}) + + with pytest.raises(ValueError): + DataTable.create( + data=df, + columns=["A"], + )