int:
"""
Calculate optimal width for a column based on content.
@@ -612,19 +628,16 @@ class DataGrid(MultipleInstance):
def handle_toggle_columns_manager(self):
logger.debug(f"toggle_columns_manager")
self._panel.set_title(side="right", title="Columns")
- self._columns_manager.adding_new_column = False
- self._columns_manager.set_all_columns(True)
- self._columns_manager.unbind_command(("ShowAllColumns", "SaveColumnDetails"))
- self._panel.set_right(self._columns_manager)
+ self._panel.set_right(self._columns_list)
def handle_toggle_new_column_editor(self):
logger.debug(f"handle_toggle_new_column_editor")
self._panel.set_title(side="right", title="Columns")
- self._columns_manager.adding_new_column = True
- self._columns_manager.set_all_columns(False)
- self._columns_manager.bind_command(("ShowAllColumns", "SaveColumnDetails"),
- self._panel.commands.set_side_visible("right", False))
- self._panel.set_right(self._columns_manager)
+ self._columns_list.adding_new_column = True
+ self._columns_list.set_all_columns(False)
+ self._columns_list.bind_command(("ShowAllColumns", "SaveColumnDetails"),
+ self._panel.commands.set_side_visible("right", False))
+ self._panel.set_right(self._columns_list)
def handle_toggle_formatting_editor(self):
logger.debug(f"toggle_formatting_editor")
@@ -670,6 +683,9 @@ class DataGrid(MultipleInstance):
manager = InstancesManager.get_by_type(self._session, DataServicesManager, None)
return manager.get_formula_engine() if manager is not None else None
+ def get_columns(self):
+ return self._columns
+
@staticmethod
def get_grid_id_from_data_service_id(data_service_id):
return data_service_id.replace(DataService.compute_prefix(), DataGrid.compute_prefix(), 1)
diff --git a/src/myfasthtml/controls/DataGridColumnsList.py b/src/myfasthtml/controls/DataGridColumnsList.py
new file mode 100644
index 0000000..b821b4a
--- /dev/null
+++ b/src/myfasthtml/controls/DataGridColumnsList.py
@@ -0,0 +1,94 @@
+import logging
+
+from fasthtml.components import *
+
+from myfasthtml.controls.BaseCommands import BaseCommands
+from myfasthtml.controls.IconsHelper import IconsHelper
+from myfasthtml.controls.Search import Search
+from myfasthtml.controls.Sortable import Sortable
+from myfasthtml.controls.datagrid_objects import DataGridColumnState
+from myfasthtml.controls.helpers import mk
+from myfasthtml.core.commands import Command
+from myfasthtml.core.instances import MultipleInstance
+from myfasthtml.icons.fluent_p1 import chevron_right20_regular
+from myfasthtml.icons.tabler import grip_horizontal
+
+logger = logging.getLogger("DataGridColumnsList")
+
+
+class Commands(BaseCommands):
+ def on_reorder(self):
+ return Command("ReorderColumns",
+ "Reorder columns in DataGrid",
+ self._owner,
+ self._owner.handle_on_reorder
+ ).htmx(target=f"#{self._id}")
+
+
+class DataGridColumnsList(MultipleInstance):
+ """
+ Show the list of columns in a DataGrid.
+ You can set the visibility of each column.
+ You can also reorder the columns via drag and drop.
+ """
+
+ def __init__(self, parent, _id=None):
+ super().__init__(parent, _id=_id)
+ self.commands = Commands(self)
+
+ @property
+ def columns(self):
+ from myfasthtml.core.constants import ColumnType
+ return [c for c in self._parent.get_columns() if c.type != ColumnType.RowSelection_]
+
+ def handle_on_reorder(self, order: list):
+ logger.debug(f"on_reorder {order=}")
+ ret = self._parent.handle_reorder_columns(order)
+ return self.render(), ret
+
+ def mk_column_label(self, col_def: DataGridColumnState):
+ return Div(
+ mk.icon(grip_horizontal, cls="mf-drag-handle cursor-grab mr-1 opacity-40"),
+ mk.mk(
+ Input(type="checkbox", cls="checkbox checkbox-sm", checked=col_def.visible),
+ # command=self.commands.toggle_column(col_def.col_id)
+ ),
+ mk.mk(
+ Div(
+ Div(mk.label(col_def.col_id, icon=IconsHelper.get(col_def.type), cls="ml-2")),
+ Div(mk.icon(chevron_right20_regular), cls="mr-2"),
+ cls="dt2-column-manager-label"
+ ),
+ # command=self.commands.show_column_details(col_def.col_id)
+ ),
+ cls="flex mb-1 items-center",
+ id=f"tcolman_{self._id}-{col_def.col_id}",
+ data_sort_id=col_def.col_id,
+ )
+
+ def mk_columns(self):
+ return Search(self,
+ items_names="Columns",
+ items=self.columns,
+ get_attr=lambda x: x.col_id,
+ get_id=lambda x: x.col_id,
+ template=self.mk_column_label,
+ max_height=None,
+ _id="-Search"
+ )
+
+ def render(self):
+ search = self.mk_columns()
+ sortable = Sortable(self,
+ command=self.commands.on_reorder(),
+ container_id=f"{search.get_id()}-results",
+ handle=".mf-drag-handle",
+ _id="-sortable")
+ return Div(search,
+ sortable,
+ id=self._id,
+ cls="pt-2",
+ style="height: 100%;")
+
+ def __ft__(self):
+ return self.render()
diff --git a/src/myfasthtml/controls/IconsHelper.py b/src/myfasthtml/controls/IconsHelper.py
index 3d5d382..10be1c7 100644
--- a/src/myfasthtml/controls/IconsHelper.py
+++ b/src/myfasthtml/controls/IconsHelper.py
@@ -54,6 +54,9 @@ class IconsHelper:
if name in IconsHelper._icons:
return IconsHelper._icons[name]
+ if not isinstance(name, str):
+ return question20_regular
+
import importlib
import pkgutil
import myfasthtml.icons as icons_pkg
@@ -76,7 +79,7 @@ class IconsHelper:
IconsHelper._icons[name] = icon
return icon
- return None
+ return question20_regular
@staticmethod
def reset():
diff --git a/src/myfasthtml/controls/Search.py b/src/myfasthtml/controls/Search.py
index ce05ea2..76af35f 100644
--- a/src/myfasthtml/controls/Search.py
+++ b/src/myfasthtml/controls/Search.py
@@ -14,12 +14,13 @@ logger = logging.getLogger("Search")
class Commands(BaseCommands):
def search(self):
- return (Command("Search",
- f"Search {self._owner.items_names}",
- self._owner,
- self._owner.on_search).htmx(target=f"#{self._owner.get_id()}-results",
- trigger="keyup changed delay:300ms",
- swap="innerHTML"))
+ return Command("Search",
+ f"Search {self._owner.items_names}",
+ self._owner,
+ self._owner.on_search).htmx(target=f"#{self._owner.get_id()}-results",
+ trigger="keyup changed delay:300ms",
+ swap="innerHTML",
+ auto_swap_oob=False)
class Search(MultipleInstance):
@@ -45,6 +46,7 @@ class Search(MultipleInstance):
items_names=None, # what is the name of the items to filter
items=None, # first set of items to filter
get_attr: Callable[[Any], str] = None, # items is a list of objects: how to get the str to filter
+ get_id: Callable[[Any], str] = None, # use for deduplication
template: Callable[[Any], Any] = None, # once filtered, what to render ?
max_height: int = 400):
"""
@@ -65,6 +67,7 @@ class Search(MultipleInstance):
self.items = items or []
self.filtered = self.items.copy()
self.get_attr = get_attr or (lambda x: x)
+ self.get_item_id = get_id
self.template = template or (lambda x: Div(self.get_attr(x)))
self.commands = Commands(self)
self.max_height = max_height
@@ -86,6 +89,7 @@ class Search(MultipleInstance):
return tuple(self._mk_search_results())
def search(self, query):
+
logger.debug(f"search {query=}")
if query is None or query.strip() == "":
self.filtered = self.items.copy()
@@ -93,24 +97,39 @@ class Search(MultipleInstance):
else:
res_seq = subsequence_matching(query, self.items, get_attr=self.get_attr)
res_fuzzy = fuzzy_matching(query, self.items, get_attr=self.get_attr)
- self.filtered = res_seq + res_fuzzy
+ self.filtered = self._unique_items(res_seq + res_fuzzy)
return self.filtered
def _mk_search_results(self):
return [self.template(item) for item in self.filtered]
+ def _unique_items(self, items: list):
+ if self.get_item_id is None:
+ return items
+
+ already_seen = set()
+ res = []
+ for item in items:
+ _id = self.get_item_id(item)
+ if _id not in already_seen:
+ already_seen.add(_id)
+ res.append(item)
+ return res
+
def render(self):
return Div(
- mk.mk(Input(name="query", id=f"{self._id}-search", type="text", placeholder="Search...", cls="input input-xs"),
+ mk.mk(Input(name="query",
+ id=f"{self._id}-search",
+ type="text", placeholder="Search...",
+ cls="input input-xs w-full"),
command=self.commands.search()),
- Div(
- *self._mk_search_results(),
- id=f"{self._id}-results",
- cls="mf-search-results",
- style="max-height: 400px;" if self.max_height else None
- ),
+ Div(*self._mk_search_results(),
+ id=f"{self._id}-results",
+ cls="mf-search-results"),
id=f"{self._id}",
+ cls="mf-search",
+ style=f"max-height: {self.max_height}px;" if self.max_height else None
)
def __ft__(self):
diff --git a/src/myfasthtml/controls/Sortable.py b/src/myfasthtml/controls/Sortable.py
new file mode 100644
index 0000000..5d95dac
--- /dev/null
+++ b/src/myfasthtml/controls/Sortable.py
@@ -0,0 +1,91 @@
+"""
+Sortable control for drag-and-drop reordering of list items.
+
+Wraps SortableJS to enable drag-and-drop on any container, posting
+the new item order to the server via HTMX after each drag operation.
+Requires SortableJS to be loaded via create_app(sortable=True).
+"""
+import logging
+from typing import Optional
+
+from fasthtml.components import Script
+
+from myfasthtml.core.commands import Command
+from myfasthtml.core.instances import MultipleInstance
+
+logger = logging.getLogger("Sortable")
+
+
+class Sortable(MultipleInstance):
+ """
+ Composable control that enables SortableJS drag-and-drop on a container.
+
+ Place this inside a render() method alongside the sortable container.
+ Items in the container must have a ``data-sort-id`` attribute identifying
+ each item. After a drag, the new order is POSTed to the server via the
+ provided command.
+
+ Args:
+ parent: Parent instance that owns this control.
+ command: Command to execute after reordering. Its handler must accept
+ an ``order: list`` parameter receiving the sorted IDs.
+ _id: Optional custom ID suffix.
+ container_id: ID of the DOM element to make sortable. Defaults to
+ ``parent.get_id()`` if not provided.
+ handle: Optional CSS selector for the drag handle within each item.
+ If None, the entire item is draggable.
+ group: Optional SortableJS group name to allow dragging between
+ multiple connected lists.
+ """
+
+ def __init__(self,
+ parent,
+ command: Command,
+ _id: Optional[str] = None,
+ container_id: Optional[str] = None,
+ handle: Optional[str] = None,
+ group: Optional[str] = None):
+ super().__init__(parent, _id=_id)
+ self._command = command
+ self._container_id = container_id
+ self._handle = handle
+ self._group = group
+
+ def render(self):
+ container_id = self._container_id or self._parent.get_id()
+ opts = self._command.ajax_htmx_options()
+
+ js_opts = ["animation: 150"]
+ if self._handle:
+ js_opts.append(f"handle: '{self._handle}'")
+ if self._group:
+ js_opts.append(f"group: '{self._group}'")
+
+ existing_values = ", ".join(f'"{k}": "{v}"' for k, v in opts["values"].items())
+
+ js_opts.append(f"""onEnd: function(evt) {{
+ var items = Array.from(document.getElementById('{container_id}').children)
+ .map(function(el) {{ return el.dataset.sortId; }})
+ .filter(Boolean);
+ htmx.ajax('POST', '{opts["url"]}', {{
+ target: '{opts["target"]}',
+ swap: '{opts["swap"]}',
+ values: {{ {existing_values}, order: items.join(',') }}
+ }});
+ }}""")
+
+ js_opts_str = ",\n ".join(js_opts)
+
+ script = f"""(function() {{
+ var container = document.getElementById('{container_id}');
+ if (!container) {{ return; }}
+ new Sortable(container, {{
+ {js_opts_str}
+ }});
+}})();"""
+
+ logger.debug(f"Sortable rendered for container={container_id}")
+ return Script(script)
+
+ def __ft__(self):
+ return self.render()
diff --git a/src/myfasthtml/core/data/DataServicesManager.py b/src/myfasthtml/core/data/DataServicesManager.py
index 7e6e168..e59a9c4 100644
--- a/src/myfasthtml/core/data/DataServicesManager.py
+++ b/src/myfasthtml/core/data/DataServicesManager.py
@@ -5,7 +5,7 @@ from myfasthtml.core.data.DataService import DataService
from myfasthtml.core.formula.engine import FormulaEngine
from myfasthtml.core.instances import SingleInstance
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("DataServicesManager")
class DataServicesManager(SingleInstance):
@@ -75,12 +75,13 @@ class DataServicesManager(SingleInstance):
Returns:
The restored DataService instance.
"""
+ logger.debug(f"restore_service {grid_id=}")
+
if grid_id in self._services:
return self._services[grid_id]
service = DataService(self, _id=grid_id)
self._services[grid_id] = service
- logger.debug("DataService restored for grid_id=%s", grid_id)
return service
def remove_service(self, grid_id: str) -> None:
diff --git a/src/myfasthtml/core/dbmanager.py b/src/myfasthtml/core/dbmanager.py
index 2a7a603..3177e99 100644
--- a/src/myfasthtml/core/dbmanager.py
+++ b/src/myfasthtml/core/dbmanager.py
@@ -47,8 +47,8 @@ class DbObject:
def __init__(self, owner: BaseInstance, name=None, db_manager=None, save_state=True):
self._owner = owner
self._name = name or owner.get_id()
- if self._name.startswith(("#", "-")) and owner.get_parent() is not None:
- self._name = owner.get_parent().get_id() + self._name
+ if self._name.startswith(("#", "-")) and owner is not None:
+ self._name = owner.get_id() + self._name
self._db_manager = db_manager or DbManager(self._owner)
self._save_state = save_state
diff --git a/src/myfasthtml/core/instances.py b/src/myfasthtml/core/instances.py
index 30c1d06..8803208 100644
--- a/src/myfasthtml/core/instances.py
+++ b/src/myfasthtml/core/instances.py
@@ -55,7 +55,7 @@ class BaseInstance:
if key in InstancesManager.instances:
res = InstancesManager.instances[key]
if type(res) is not cls:
- raise TypeError(f"Instance with id {_id} already exists, but is of type {type(res)}")
+ raise TypeError(f"Instance with id {_id} already exists, but is of type {type(res)} (instead of {cls})")
if VERBOSE_VERBOSE:
logger.debug(f" instance {_id} already exists, returning existing instance")
diff --git a/src/myfasthtml/myfastapp.py b/src/myfasthtml/myfastapp.py
index 4677458..6fb8959 100644
--- a/src/myfasthtml/myfastapp.py
+++ b/src/myfasthtml/myfastapp.py
@@ -78,6 +78,7 @@ def include_assets(module_name: str, order: Optional[List[str]] = None) -> list:
def create_app(daisyui: Optional[bool] = True,
vis: Optional[bool] = True,
code_mirror: Optional[bool] = True,
+ sortable: Optional[bool] = True,
protect_routes: Optional[bool] = True,
mount_auth_app: Optional[bool] = False,
base_url: Optional[str] = None,
@@ -85,16 +86,19 @@ def create_app(daisyui: Optional[bool] = True,
"""
Creates and configures a FastHtml application with optional support for daisyUI themes and
authentication routes.
-
+
:param daisyui: Flag to enable or disable inclusion of daisyUI (https://daisyui.com/).
Defaults to True.
-
+
:param vis: Flag to enable or disable inclusion of Vis network (https://visjs.org/)
Defaults to True.
-
+
:param code_mirror: Flag to enable or disable inclusion of Code Mirror (https://codemirror.net/)
Defaults to True.
-
+
+ :param sortable: Flag to enable or disable inclusion of SortableJS (https://sortablejs.github.io/Sortable/).
+ Defaults to False.
+
:param protect_routes: Flag to enable or disable routes protection based on authentication.
Defaults to True.
:param mount_auth_app: Flag to enable or disable mounting of authentication routes.
@@ -116,6 +120,9 @@ def create_app(daisyui: Optional[bool] = True,
if code_mirror:
hdrs += include_assets("codemirror", order=["codemirror"])
+
+ if sortable:
+ hdrs += include_assets("sortableJs")
beforeware = create_auth_beforeware() if protect_routes else None
app, rt = fasthtml.fastapp.fast_app(before=beforeware, hdrs=tuple(hdrs), **kwargs)
diff --git a/src/myfasthtml/test/matcher.py b/src/myfasthtml/test/matcher.py
index 2199966..1581a19 100644
--- a/src/myfasthtml/test/matcher.py
+++ b/src/myfasthtml/test/matcher.py
@@ -276,11 +276,14 @@ def _get_attr(x, attr):
if isinstance(x, NotStr) and attr == "s":
# Special case for NotStr: return the name of the svg
- svg = getattr(x, attr, MISSING_ATTR)
- match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', svg)
- if match:
- return f''
-
+ attr_value = getattr(x, attr, MISSING_ATTR)
+ if attr_value.strip().startswith("'
+ else:
+ return attr_value
+
return getattr(x, attr, MISSING_ATTR)
diff --git a/tests/controls/test_datagrid.py b/tests/controls/test_datagrid.py
index 414ccf8..29922f9 100644
--- a/tests/controls/test_datagrid.py
+++ b/tests/controls/test_datagrid.py
@@ -1,5 +1,6 @@
import pandas as pd
import pytest
+from fastcore.basics import NotStr
from fasthtml.components import Div, Script
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
@@ -785,6 +786,22 @@ class TestDataGridRender:
# Body rows
# ------------------------------------------------------------------
+ def test_i_can_render_body(self, datagrid_with_data):
+ """Test that the body renders with the correct ID and CSS class.
+ """
+ dg = datagrid_with_data
+ html = dg.mk_body_wrapper()
+ expected = Div(
+ Div(
+ Div(cls="dt2-row"),
+ Div(cls="dt2-row"),
+ Div(cls="dt2-row"),
+ cls=Contains("dt2-body")),
+ id=f"tb_{dg._id}",
+ cls=Contains("dt2-body-container"),
+ )
+ assert matches(html, expected)
+
def test_i_can_render_body_row_count(self, datagrid_with_full_data):
"""Test that mk_body_content_page returns one row per DataFrame row plus the add-row button.
@@ -922,13 +939,13 @@ class TestDataGridRender:
# Body cell content
# ------------------------------------------------------------------
- @pytest.mark.parametrize("col_title, expected_css_class, expected_value", [
- ("name", "dt2-cell-content-text", "Alice"),
- ("age", "dt2-cell-content-number", "25"),
- ("active", "dt2-cell-content-checkbox", None),
+ @pytest.mark.parametrize("col_title, expected_value", [
+ ("name", 'Alice'),
+ ("age", '25'),
+ ("active", ''),
])
def test_i_can_render_body_cell_content_for_column_type(
- self, datagrid_with_full_data, col_title, expected_css_class, expected_value):
+ self, datagrid_with_full_data, col_title, expected_value):
"""Test that cell content carries the correct CSS class and value for each column type.
Why these elements matter:
@@ -945,11 +962,4 @@ class TestDataGridRender:
col_pos = dg._columns.index(col_def)
content = dg.mk_body_cell_content(col_pos, 0, col_def, None)
-
- assert expected_css_class in str(content), (
- f"Expected CSS class '{expected_css_class}' in cell content for column '{col_title}'"
- )
- if expected_value is not None:
- assert expected_value in str(content), (
- f"Expected value '{expected_value}' in cell content for column '{col_title}'"
- )
+ assert matches(content, NotStr(expected_value))
diff --git a/tests/controls/test_datagrid_columns_manager.py b/tests/controls/test_datagrid_columns_manager.py
index e206f48..56c2202 100644
--- a/tests/controls/test_datagrid_columns_manager.py
+++ b/tests/controls/test_datagrid_columns_manager.py
@@ -6,8 +6,9 @@ from fasthtml.common import Div, FT, Input, Form, Fieldset, Select
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
from myfasthtml.controls.Search import Search
-from myfasthtml.controls.datagrid_objects import DataGridColumnState
+from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridColumnUiState
from myfasthtml.core.constants import ColumnType
+from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
from myfasthtml.core.instances import InstancesManager, MultipleInstance
from myfasthtml.test.matcher import (
matches, find_one, find, Contains, TestIcon, TestObject
@@ -41,13 +42,18 @@ class MockDataGrid(MultipleInstance):
return None
+# col_def: ColumnDefinition, col_ui_state: DataGridColumnUiState
+
@pytest.fixture
def mock_datagrid(root_instance):
"""Create a mock DataGrid with sample columns."""
columns = [
- DataGridColumnState(col_id="name", col_index=0, title="Name", type=ColumnType.Text, visible=True),
- DataGridColumnState(col_id="age", col_index=1, title="Age", type=ColumnType.Number, visible=True),
- DataGridColumnState(col_id="email", col_index=2, title="Email", type=ColumnType.Text, visible=False),
+ DataGridColumnState(ColumnDefinition(col_id="name", col_index=0, title="Name", type=ColumnType.Text),
+ DataGridColumnUiState(col_id="name", visible=True)),
+ DataGridColumnState(ColumnDefinition(col_id="age", col_index=1, title="Age", type=ColumnType.Number),
+ DataGridColumnUiState(col_id="age", visible=True)),
+ DataGridColumnState(ColumnDefinition(col_id="email", col_index=2, title="Email", type=ColumnType.Text),
+ DataGridColumnUiState(col_id="email", visible=False)),
]
yield MockDataGrid(root_instance, columns=columns, _id="test-datagrid")
InstancesManager.reset()
diff --git a/tests/controls/test_datagrid_formatting.py b/tests/controls/test_datagrid_formatting.py
index b80bb79..3a283a5 100644
--- a/tests/controls/test_datagrid_formatting.py
+++ b/tests/controls/test_datagrid_formatting.py
@@ -9,12 +9,14 @@ import pytest
from myfasthtml.controls.DataGrid import DataGrid
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
from myfasthtml.controls.DataGridsManager import DataGridsManager
-from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowUiState
+from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridColumnUiState
from myfasthtml.core.constants import ColumnType
+from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
from myfasthtml.core.formatting.dataclasses import FormatRule, Style
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
from myfasthtml.core.instances import InstancesManager
+
@pytest.fixture
def manager(root_instance):
"""Create a DataGridsManager instance."""
@@ -27,19 +29,16 @@ def manager(root_instance):
def datagrid(manager):
"""Create a DataGrid instance."""
from myfasthtml.controls.DataGrid import DatagridConf
- conf = DatagridConf(namespace="app", name="products", id="test-grid")
- grid = DataGrid(manager, conf=conf, save_state=False, _id="test-datagrid")
+ conf = DatagridConf(namespace="app", name="products")
+ grid = DataGrid(manager, conf=conf, save_state=False, _id="mf-data_grid-test-datagrid")
+ # ColumnDefinition, col_ui_state: DataGridColumnUiState
# Add some columns
- grid._state.columns = [
- DataGridColumnState(col_id="amount", col_index=0, title="Amount", type=ColumnType.Number, visible=True),
- DataGridColumnState(col_id="status", col_index=1, title="Status", type=ColumnType.Text, visible=True),
- ]
-
- # Add some rows
- grid._state.rows = [
- DataGridRowUiState(0),
- DataGridRowUiState(1),
+ grid.columns = [
+ DataGridColumnState(ColumnDefinition(col_id="amount", col_index=0, title="Amount", type=ColumnType.Number),
+ DataGridColumnUiState(col_id="amount", visible=True)),
+ DataGridColumnState(ColumnDefinition(col_id="status", col_index=1, title="Status", type=ColumnType.Text),
+ DataGridColumnUiState(col_id="status", visible=True))
]
yield grid