I can format columns
This commit is contained in:
@@ -24,7 +24,6 @@ from myfasthtml.controls.helpers import mk, icons
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.formatting.dataclasses import FormatRule, Style, Condition, ConstantFormatter
|
||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
@@ -181,7 +180,7 @@ class DataGrid(MultipleInstance):
|
||||
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
||||
|
||||
# add Panel
|
||||
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="#panel")
|
||||
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
|
||||
self._panel.set_side_visible("right", False) # the right Panel always starts closed
|
||||
self.bind_command("ToggleColumnsManager", self._panel.commands.toggle_side("right"))
|
||||
self.bind_command("ToggleFormattingEditor", self._panel.commands.toggle_side("right"))
|
||||
@@ -212,7 +211,7 @@ class DataGrid(MultipleInstance):
|
||||
conf=editor_conf,
|
||||
dsl=FormattingDSL(),
|
||||
save_state=self._settings.save_state,
|
||||
_id="#formatting_editor")
|
||||
_id="-formatting_editor")
|
||||
|
||||
# other definitions
|
||||
self._mouse_support = {
|
||||
@@ -398,17 +397,6 @@ class DataGrid(MultipleInstance):
|
||||
list[FormatRule] or None if no formatting defined
|
||||
"""
|
||||
|
||||
# hack to test
|
||||
if col_def.col_id == "age":
|
||||
return [
|
||||
FormatRule(style=Style(color="green")),
|
||||
FormatRule(condition=Condition(operator=">", value=18), style=Style(color="red")),
|
||||
]
|
||||
|
||||
return [
|
||||
FormatRule(condition=Condition(operator="isnan"), formatter=ConstantFormatter(value="-")),
|
||||
]
|
||||
|
||||
cell_id = self._get_element_id_from_pos("cell", (col_pos, row_index))
|
||||
|
||||
if cell_id in self._state.cell_formats:
|
||||
@@ -816,7 +804,7 @@ class DataGrid(MultipleInstance):
|
||||
cls="flex items-center justify-between mb-2"),
|
||||
self._panel.set_main(self.mk_table_wrapper()),
|
||||
Script(f"initDataGrid('{self._id}');"),
|
||||
Mouse(self, combinations=self._mouse_support),
|
||||
Mouse(self, combinations=self._mouse_support, _id="-mouse"),
|
||||
id=self._id,
|
||||
cls="grid",
|
||||
style="height: 100%; grid-template-rows: auto 1fr;"
|
||||
|
||||
@@ -1,7 +1,112 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from myfasthtml.controls.DslEditor import DslEditor
|
||||
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope
|
||||
|
||||
logger = logging.getLogger("DataGridFormattingEditor")
|
||||
|
||||
|
||||
class DataGridFormattingEditor(DslEditor):
|
||||
|
||||
def on_dsl_change(self, dsl):
|
||||
pass
|
||||
def _find_column_by_name(self, name: str):
|
||||
"""
|
||||
Find a column by name, searching col_id first, then title.
|
||||
|
||||
Returns:
|
||||
tuple (col_pos, col_def) if found, (None, None) otherwise
|
||||
"""
|
||||
# First pass: match by col_id
|
||||
for col_pos, col_def in enumerate(self._parent.get_state().columns):
|
||||
if col_def.col_id == name:
|
||||
return col_pos, col_def
|
||||
|
||||
# Second pass: match by title
|
||||
for col_pos, col_def in enumerate(self._parent.get_state().columns):
|
||||
if col_def.title == name:
|
||||
return col_pos, col_def
|
||||
|
||||
return None, None
|
||||
|
||||
def _get_cell_id(self, scope: CellScope):
|
||||
"""
|
||||
Get cell_id from CellScope.
|
||||
|
||||
If scope has cell_id, use it directly.
|
||||
Otherwise, resolve coordinates (column, row) to cell_id.
|
||||
|
||||
Returns:
|
||||
cell_id string or None if column not found
|
||||
"""
|
||||
if scope.cell_id:
|
||||
return scope.cell_id
|
||||
|
||||
col_pos, _ = self._find_column_by_name(scope.column)
|
||||
if col_pos is None:
|
||||
logger.warning(f"Column '{scope.column}' not found for CellScope")
|
||||
return None
|
||||
|
||||
return self._parent._get_element_id_from_pos("cell", (col_pos, scope.row))
|
||||
|
||||
def on_content_changed(self):
|
||||
dsl = self.get_content()
|
||||
|
||||
# Step 1: Parse DSL
|
||||
try:
|
||||
scoped_rules = parse_dsl(dsl)
|
||||
except DSLSyntaxError as e:
|
||||
logger.debug(f"DSL syntax error, keeping old formatting: {e}")
|
||||
return
|
||||
|
||||
# Step 2: Group rules by scope
|
||||
columns_rules = defaultdict(list) # key = column name
|
||||
rows_rules = defaultdict(list) # key = row index
|
||||
cells_rules = defaultdict(list) # key = cell_id
|
||||
|
||||
for scoped_rule in scoped_rules:
|
||||
scope = scoped_rule.scope
|
||||
rule = scoped_rule.rule
|
||||
|
||||
if isinstance(scope, ColumnScope):
|
||||
columns_rules[scope.column].append(rule)
|
||||
elif isinstance(scope, RowScope):
|
||||
rows_rules[scope.row].append(rule)
|
||||
elif isinstance(scope, CellScope):
|
||||
cell_id = self._get_cell_id(scope)
|
||||
if cell_id:
|
||||
cells_rules[cell_id].append(rule)
|
||||
|
||||
# Step 3: Copy state for atomic update
|
||||
state = self._parent.get_state().copy()
|
||||
|
||||
# Step 4: Clear existing formats on the copy
|
||||
for col in state.columns:
|
||||
col.format = None
|
||||
for row in state.rows:
|
||||
row.format = None
|
||||
state.cell_formats.clear()
|
||||
|
||||
# Step 5: Apply grouped rules on the copy
|
||||
for column_name, rules in columns_rules.items():
|
||||
col_pos, col_def = self._find_column_by_name(column_name)
|
||||
if col_def:
|
||||
# Find the column in the copied state
|
||||
state.columns[col_pos].format = rules
|
||||
else:
|
||||
logger.warning(f"Column '{column_name}' not found, skipping rules")
|
||||
|
||||
for row_index, rules in rows_rules.items():
|
||||
if row_index < len(state.rows):
|
||||
state.rows[row_index].format = rules
|
||||
else:
|
||||
logger.warning(f"Row {row_index} out of range, skipping rules")
|
||||
|
||||
for cell_id, rules in cells_rules.items():
|
||||
state.cell_formats[cell_id] = rules
|
||||
|
||||
# Step 6: Update state atomically
|
||||
self._parent.get_state().update(state)
|
||||
|
||||
# Step 7: Refresh the DataGrid
|
||||
logger.debug(f"Formatting applied: {len(columns_rules)} columns, {len(rows_rules)} rows, {len(cells_rules)} cells")
|
||||
return self._parent.render_partial("body")
|
||||
|
||||
@@ -62,11 +62,11 @@ class Commands(BaseCommands):
|
||||
self._owner,
|
||||
self._owner.toggle_auto_save).htmx(target=f"#as_{self._id}", trigger="click")
|
||||
|
||||
def on_content_changed(self):
|
||||
return Command("OnContentChanged",
|
||||
"On content changed",
|
||||
def save_content(self):
|
||||
return Command("SaveContent",
|
||||
"Save content",
|
||||
self._owner,
|
||||
self._owner.on_content_changed
|
||||
self._owner.save_content
|
||||
).htmx(target=None)
|
||||
|
||||
|
||||
@@ -112,15 +112,24 @@ class DslEditor(MultipleInstance):
|
||||
"""Get the current editor content."""
|
||||
return self._state.content
|
||||
|
||||
def update_content(self, content: str = "") -> None:
|
||||
def update_content(self, content: str = ""):
|
||||
"""Handler for content update from CodeMirror."""
|
||||
self._state.content = content
|
||||
if self._state.auto_save:
|
||||
self.on_content_changed()
|
||||
logger.debug(f"Content updated: {len(content)} chars")
|
||||
|
||||
if self._state.auto_save:
|
||||
return None, self.on_content_changed() # on_content_changed must be second to benefit from oob swap
|
||||
|
||||
return None
|
||||
|
||||
def save_content(self):
|
||||
logger.debug("save_content")
|
||||
return None, self.on_content_changed() # on_content_changed must be second to benefit from oob swap
|
||||
|
||||
def toggle_auto_save(self):
|
||||
logger.debug("toggle_auto_save")
|
||||
self._state.auto_save = not self._state.auto_save
|
||||
logger.debug(f" auto_save={self._state.auto_save}")
|
||||
return self._mk_auto_save()
|
||||
|
||||
def on_content_changed(self) -> None:
|
||||
@@ -182,7 +191,7 @@ class DslEditor(MultipleInstance):
|
||||
mk.button("Save",
|
||||
cls="btn btn-xs btn-primary",
|
||||
disabled="disabled" if self._state.auto_save else None,
|
||||
command=self.commands.update_content()),
|
||||
command=self.commands.save_content()),
|
||||
cls="flex justify-between items-center p-2",
|
||||
id=f"as_{self._id}",
|
||||
),
|
||||
|
||||
@@ -21,7 +21,9 @@ class InstancesDebugger(SingleInstance):
|
||||
return self._panel.set_main(vis_network)
|
||||
|
||||
def on_network_event(self, event_data: dict):
|
||||
session, instance_id = event_data["nodes"][0].split("#")
|
||||
parts = event_data["nodes"][0].split("#")
|
||||
session = parts[0]
|
||||
instance_id = "#".join(parts[1:])
|
||||
properties_def = {"Main": {"Id": "_id", "Parent Id": "_parent._id"},
|
||||
"State": {"_name": "_state._name", "*": "_state"},
|
||||
"Commands": {"*": "commands"},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional
|
||||
|
||||
@@ -12,6 +13,7 @@ from myfasthtml.core.instances import MultipleInstance
|
||||
from myfasthtml.icons.fluent_p1 import more_horizontal20_regular
|
||||
from myfasthtml.icons.fluent_p2 import subtract20_regular
|
||||
|
||||
logger = logging.getLogger("Panel")
|
||||
|
||||
class PanelIds:
|
||||
def __init__(self, owner):
|
||||
@@ -116,6 +118,7 @@ class Panel(MultipleInstance):
|
||||
return self._ids
|
||||
|
||||
def update_side_width(self, side, width):
|
||||
logger.debug(f"update_side_width {side=} {width=}")
|
||||
if side == "left":
|
||||
self._state.left_width = width
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user