diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index 7217df9..fe7c4c0 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -233,7 +233,7 @@ class DataGrid(MultipleInstance): # add columns manager self._columns_manager = DataGridColumnsManager(self) 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: 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_row_data = _init_row_data(self._df) 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 @@ -677,8 +676,27 @@ class DataGrid(MultipleInstance): return Span(*res, cls=f"{css_class} truncate", style=style) if len(res) > 1 else res[0] 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('') + 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'{html.escape(value)}' + ) + # 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) if column_type == ColumnType.Bool: return _mk_bool_cached(value) @@ -722,8 +740,9 @@ class DataGrid(MultipleInstance): """ if not col_def.visible: 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) return OptimizedDiv(content, diff --git a/src/myfasthtml/controls/DataGridColumnsManager.py b/src/myfasthtml/controls/DataGridColumnsManager.py index 9c3d437..347f030 100644 --- a/src/myfasthtml/controls/DataGridColumnsManager.py +++ b/src/myfasthtml/controls/DataGridColumnsManager.py @@ -41,8 +41,8 @@ class Commands(BaseCommands): self._owner.show_all_columns).htmx(target=f"#{self._id}", swap="innerHTML") def save_column_details(self, col_id): - return Command(f"UpdateColumn", - f"Update column {col_id}", + return Command(f"SaveColumnDetails", + f"Save column {col_id}", self._owner, self._owner.save_column_details, kwargs={"col_id": col_id} @@ -71,7 +71,7 @@ class DataGridColumnsManager(MultipleInstance): self._parent._parent, 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") DslsManager.register(completion_engine, FormulaParser()) @@ -102,7 +102,7 @@ class DataGridColumnsManager(MultipleInstance): col_def.width = int(v) elif k == "formula": col_def.formula = v or "" - self._register_formula(col_def) + # self._register_formula(col_def), Will be done in save_column_details() else: setattr(col_def, k, v) @@ -133,7 +133,8 @@ class DataGridColumnsManager(MultipleInstance): def save_column_details(self, 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() return self._mk_inner_content() @@ -149,12 +150,17 @@ class DataGridColumnsManager(MultipleInstance): return self.mk_column_details(col_def) 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() if engine is None: return table = self._parent.get_table_name() - if col_def.formula: + if col_def.type == ColumnType.Formula and col_def.formula: try: engine.set_formula(table, col_def.col_id, col_def.formula) logger.debug("Registered formula for %s.%s", table, col_def.col_id) diff --git a/src/myfasthtml/controls/DslEditor.py b/src/myfasthtml/controls/DslEditor.py index 6129e2f..7eda73b 100644 --- a/src/myfasthtml/controls/DslEditor.py +++ b/src/myfasthtml/controls/DslEditor.py @@ -167,7 +167,7 @@ class DslEditor(MultipleInstance): return Textarea( self._state.content, 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", ) diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py index c8d1771..dcd2e3d 100644 --- a/src/myfasthtml/controls/helpers.py +++ b/src/myfasthtml/controls/helpers.py @@ -7,7 +7,7 @@ from myfasthtml.core.utils import merge_classes from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_row20_regular, \ number_symbol20_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_p3 import calendar_ltr20_regular @@ -160,4 +160,5 @@ icons = { ColumnType.Datetime: calendar_ltr20_regular, ColumnType.Bool: checkbox_checked20_filled, ColumnType.Enum: text_bullet_list_square20_regular, + ColumnType.Formula: math_formula16_regular, } diff --git a/src/myfasthtml/core/formula/engine.py b/src/myfasthtml/core/formula/engine.py index 69367ce..9aa7af8 100644 --- a/src/myfasthtml/core/formula/engine.py +++ b/src/myfasthtml/core/formula/engine.py @@ -96,7 +96,9 @@ class FormulaEngine: # Registers in DAG and raises FormulaCycleError if cycle detected self._graph.add_formula(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) def remove_formula(self, table: str, col: str) -> None: diff --git a/tests/controls/test_datagrid_columns_manager.py b/tests/controls/test_datagrid_columns_manager.py index 43f3699..f8f84af 100644 --- a/tests/controls/test_datagrid_columns_manager.py +++ b/tests/controls/test_datagrid_columns_manager.py @@ -37,6 +37,9 @@ class MockDataGrid(MultipleInstance): def get_table_name(self): return "mock_table" + def get_formula_engine(self): + return None + @pytest.fixture def mock_datagrid(root_instance):