I can apply formulas

This commit is contained in:
2026-02-15 19:55:22 +01:00
parent 27f12b2c32
commit f3e19743c8
6 changed files with 48 additions and 17 deletions

View File

@@ -233,7 +233,7 @@ class DataGrid(MultipleInstance):
# add columns manager # add columns manager
self._columns_manager = DataGridColumnsManager(self) self._columns_manager = DataGridColumnsManager(self)
self._columns_manager.bind_command("ToggleColumn", self.commands.on_column_changed()) self._columns_manager.bind_command("ToggleColumn", self.commands.on_column_changed())
self._columns_manager.bind_command("UpdateColumn", self.commands.on_column_changed()) self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed())
if self._settings.enable_formatting: if self._settings.enable_formatting:
completion_engine = FormattingCompletionEngine(self._parent, self.get_table_name()) completion_engine = FormattingCompletionEngine(self._parent, self.get_table_name())
@@ -418,8 +418,7 @@ class DataGrid(MultipleInstance):
self._df_store.ns_fast_access = _init_fast_access(self._df) self._df_store.ns_fast_access = _init_fast_access(self._df)
self._df_store.ns_row_data = _init_row_data(self._df) self._df_store.ns_row_data = _init_row_data(self._df)
self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0 self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0
if init_state: self._register_existing_formulas()
self._register_existing_formulas()
return self return self
@@ -677,8 +676,27 @@ class DataGrid(MultipleInstance):
return Span(*res, cls=f"{css_class} truncate", style=style) if len(res) > 1 else res[0] return Span(*res, cls=f"{css_class} truncate", style=style) if len(res) > 1 else res[0]
column_type = col_def.type column_type = col_def.type
value = self._df_store.ns_fast_access[col_def.col_id][row_index]
# Formula column: safe read — ns_fast_access entry may not exist yet if the
# engine hasn't run its first recalculation pass.
if column_type == ColumnType.Formula:
col_array = self._df_store.ns_fast_access.get(col_def.col_id)
if col_array is None or row_index >= len(col_array):
return NotStr('<span class="dt2-cell-content-text truncate">—</span>')
value = col_array[row_index]
# Display error strings produced by the evaluator (#ERR, #DIV/0!, …)
if isinstance(value, str) and value.startswith("#"):
return NotStr(
f'<span class="dt2-cell-content-error truncate">{html.escape(value)}</span>'
)
# Infer number vs text style from the actual computed value type
if isinstance(value, (int, float)) and not isinstance(value, bool):
column_type = ColumnType.Number
else:
column_type = ColumnType.Text
else:
value = self._df_store.ns_fast_access[col_def.col_id][row_index]
# Boolean type - uses cached HTML (only 2 possible values) # Boolean type - uses cached HTML (only 2 possible values)
if column_type == ColumnType.Bool: if column_type == ColumnType.Bool:
return _mk_bool_cached(value) return _mk_bool_cached(value)
@@ -722,8 +740,9 @@ class DataGrid(MultipleInstance):
""" """
if not col_def.visible: if not col_def.visible:
return None return None
value = self._df_store.ns_fast_access[col_def.col_id][row_index] col_array = self._df_store.ns_fast_access.get(col_def.col_id)
value = col_array[row_index] if col_array is not None and row_index < len(col_array) else None
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower) content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
return OptimizedDiv(content, return OptimizedDiv(content,

View File

@@ -41,8 +41,8 @@ class Commands(BaseCommands):
self._owner.show_all_columns).htmx(target=f"#{self._id}", swap="innerHTML") self._owner.show_all_columns).htmx(target=f"#{self._id}", swap="innerHTML")
def save_column_details(self, col_id): def save_column_details(self, col_id):
return Command(f"UpdateColumn", return Command(f"SaveColumnDetails",
f"Update column {col_id}", f"Save column {col_id}",
self._owner, self._owner,
self._owner.save_column_details, self._owner.save_column_details,
kwargs={"col_id": col_id} kwargs={"col_id": col_id}
@@ -71,7 +71,7 @@ class DataGridColumnsManager(MultipleInstance):
self._parent._parent, self._parent._parent,
self._parent.get_table_name(), self._parent.get_table_name(),
) )
conf = DslEditorConf(save_button=False, line_numbers=False, engine_id=completion_engine.get_id()) conf = DslEditorConf(name="formula", save_button=False, line_numbers=False, engine_id=completion_engine.get_id())
self._formula_editor = DataGridFormulaEditor(self, conf=conf, _id=f"{self._id}-formula-editor") self._formula_editor = DataGridFormulaEditor(self, conf=conf, _id=f"{self._id}-formula-editor")
DslsManager.register(completion_engine, FormulaParser()) DslsManager.register(completion_engine, FormulaParser())
@@ -102,7 +102,7 @@ class DataGridColumnsManager(MultipleInstance):
col_def.width = int(v) col_def.width = int(v)
elif k == "formula": elif k == "formula":
col_def.formula = v or "" col_def.formula = v or ""
self._register_formula(col_def) # self._register_formula(col_def), Will be done in save_column_details()
else: else:
setattr(col_def, k, v) setattr(col_def, k, v)
@@ -133,7 +133,8 @@ class DataGridColumnsManager(MultipleInstance):
def save_column_details(self, col_id, client_response): def save_column_details(self, col_id, client_response):
logger.debug(f"save_column_details {col_id=}, {client_response=}") logger.debug(f"save_column_details {col_id=}, {client_response=}")
self._get_updated_col_def_from_col_id(col_id, client_response, copy=False) col_def = self._get_updated_col_def_from_col_id(col_id, client_response, copy=False)
self._register_formula(col_def)
self._parent.save_state() self._parent.save_state()
return self._mk_inner_content() return self._mk_inner_content()
@@ -149,12 +150,17 @@ class DataGridColumnsManager(MultipleInstance):
return self.mk_column_details(col_def) return self.mk_column_details(col_def)
def _register_formula(self, col_def) -> None: def _register_formula(self, col_def) -> None:
"""Register or remove a formula column with the FormulaEngine.""" """Register or remove a formula column with the FormulaEngine.
Registers only when col_def.type is Formula and the formula text is
non-empty. Removes the formula in all other cases so the engine stays
consistent with the column definition.
"""
engine = self._parent.get_formula_engine() engine = self._parent.get_formula_engine()
if engine is None: if engine is None:
return return
table = self._parent.get_table_name() table = self._parent.get_table_name()
if col_def.formula: if col_def.type == ColumnType.Formula and col_def.formula:
try: try:
engine.set_formula(table, col_def.col_id, col_def.formula) engine.set_formula(table, col_def.col_id, col_def.formula)
logger.debug("Registered formula for %s.%s", table, col_def.col_id) logger.debug("Registered formula for %s.%s", table, col_def.col_id)

View File

@@ -167,7 +167,7 @@ class DslEditor(MultipleInstance):
return Textarea( return Textarea(
self._state.content, self._state.content,
id=f"ta_{self._id}", id=f"ta_{self._id}",
name=f"ta_{self._id}", name=self.conf.name if (self.conf and self.conf.name) else f"ta_{self._id}",
cls="hidden", cls="hidden",
) )

View File

@@ -7,7 +7,7 @@ from myfasthtml.core.utils import merge_classes
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_row20_regular, \ from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_row20_regular, \
number_symbol20_regular number_symbol20_regular
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \ from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
checkbox_checked20_filled checkbox_checked20_filled, math_formula16_regular
from myfasthtml.icons.fluent_p2 import text_bullet_list_square20_regular, text_field20_regular from myfasthtml.icons.fluent_p2 import text_bullet_list_square20_regular, text_field20_regular
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
@@ -160,4 +160,5 @@ icons = {
ColumnType.Datetime: calendar_ltr20_regular, ColumnType.Datetime: calendar_ltr20_regular,
ColumnType.Bool: checkbox_checked20_filled, ColumnType.Bool: checkbox_checked20_filled,
ColumnType.Enum: text_bullet_list_square20_regular, ColumnType.Enum: text_bullet_list_square20_regular,
ColumnType.Formula: math_formula16_regular,
} }

View File

@@ -96,7 +96,9 @@ class FormulaEngine:
# Registers in DAG and raises FormulaCycleError if cycle detected # Registers in DAG and raises FormulaCycleError if cycle detected
self._graph.add_formula(table, col, formula) self._graph.add_formula(table, col, formula)
self._formulas[(table, col)] = formula self._formulas[(table, col)] = formula
# Mark dirty so the column is computed on the next render
self._graph.mark_dirty(table, col)
logger.debug("Formula set for %s.%s: %s", table, col, formula_text) logger.debug("Formula set for %s.%s: %s", table, col, formula_text)
def remove_formula(self, table: str, col: str) -> None: def remove_formula(self, table: str, col: str) -> None:

View File

@@ -37,6 +37,9 @@ class MockDataGrid(MultipleInstance):
def get_table_name(self): def get_table_name(self):
return "mock_table" return "mock_table"
def get_formula_engine(self):
return None
@pytest.fixture @pytest.fixture
def mock_datagrid(root_instance): def mock_datagrid(root_instance):