I can add new columns

This commit is contained in:
2026-02-16 21:57:39 +01:00
parent f3e19743c8
commit 70915b2691
7 changed files with 142 additions and 78 deletions

View File

@@ -712,7 +712,6 @@ function updateDatagridSelection(datagridId) {
});
}
/**
* Find the parent element with .dt2-cell class and return its id.
* Used with hx-vals="js:getCellId()" for DataGrid cell identification.

View File

@@ -21,7 +21,7 @@ from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.Panel import Panel, PanelConf
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
from myfasthtml.controls.helpers import mk, icons
from myfasthtml.controls.helpers import mk, icons, column_type_defaults
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
@@ -32,7 +32,7 @@ from myfasthtml.core.formatting.dsl.parser import DSLParser
from myfasthtml.core.formatting.engine import FormattingEngine
from myfasthtml.core.instances import MultipleInstance
from myfasthtml.core.optimized_ft import OptimizedDiv
from myfasthtml.core.utils import make_safe_id, merge_classes
from myfasthtml.core.utils import make_safe_id, merge_classes, make_unique_safe_id
from myfasthtml.icons.carbon import row, column, grid
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular, column_edit20_regular
@@ -342,6 +342,74 @@ class DataGrid(MultipleInstance):
self._state.selection.selected = pos
self._state.save()
def _register_existing_formulas(self) -> None:
"""
Re-register all formula columns with the FormulaEngine.
Called after data reload to ensure the engine knows about all
formula columns and their expressions.
"""
engine = self.get_formula_engine()
if engine is None:
return
table = self.get_table_name()
for col_def in self._state.columns:
if col_def.formula:
try:
engine.set_formula(table, col_def.col_id, col_def.formula)
except Exception as e:
logger.warning("Failed to register formula for %s.%s: %s", table, col_def.col_id, e)
def _recalculate_formulas(self) -> None:
"""
Recalculate dirty formula columns before rendering.
Called at the start of mk_body_content_page() to ensure formula
columns are up-to-date before cells are rendered.
"""
engine = self.get_formula_engine()
if engine is None:
return
engine.recalculate_if_needed(self.get_table_name(), self._df_store)
def _get_format_rules(self, col_pos, row_index, col_def):
"""
Get format rules for a cell, returning only the most specific level defined.
Priority (most specific wins):
1. Cell-level: self._state.cell_formats[cell_id]
2. Row-level: row_state.format (if row has specific state)
3. Column-level: col_def.format
4. Table-level: self._state.table_format
5. Tables-level (global): manager.all_tables_formats
Args:
col_pos: Column position index
row_index: Row index
col_def: DataGridColumnState for the column
Returns:
list[FormatRule] or None if no formatting defined
"""
cell_id = self._get_element_id_from_pos("cell", (col_pos, row_index))
if cell_id in self._state.cell_formats:
return self._state.cell_formats[cell_id]
row_state = next((r for r in self._state.rows if r.row_id == row_index), None)
if row_state and row_state.format:
return row_state.format
if col_def.format:
return col_def.format
if self._state.table_format:
return self._state.table_format
# Get global tables formatting from manager
return self._parent.all_tables_formats
def init_from_dataframe(self, df, init_state=True):
def _get_column_type(dtype):
@@ -422,73 +490,36 @@ class DataGrid(MultipleInstance):
return self
def _register_existing_formulas(self) -> None:
"""
Re-register all formula columns with the FormulaEngine.
def add_new_column(self, col_def: DataGridColumnState) -> None:
"""Add a new column to the DataGrid.
Called after data reload to ensure the engine knows about all
formula columns and their expressions.
"""
engine = self.get_formula_engine()
if engine is None:
return
table = self.get_table_name()
for col_def in self._state.columns:
if col_def.formula:
try:
engine.set_formula(table, col_def.col_id, col_def.formula)
except Exception as e:
logger.warning("Failed to register formula for %s.%s: %s", table, col_def.col_id, e)
def _recalculate_formulas(self) -> None:
"""
Recalculate dirty formula columns before rendering.
For Formula columns, only _state.columns is updated; the FormulaEngine
computes values on demand via recalculate_if_needed().
Called at the start of mk_body_content_page() to ensure formula
columns are up-to-date before cells are rendered.
"""
engine = self.get_formula_engine()
if engine is None:
return
engine.recalculate_if_needed(self.get_table_name(), self._df_store)
def _get_format_rules(self, col_pos, row_index, col_def):
"""
Get format rules for a cell, returning only the most specific level defined.
Priority (most specific wins):
1. Cell-level: self._state.cell_formats[cell_id]
2. Row-level: row_state.format (if row has specific state)
3. Column-level: col_def.format
4. Table-level: self._state.table_format
5. Tables-level (global): manager.all_tables_formats
For other column types, also adds the column to the DataFrame and updates
ns_fast_access and ns_row_data incrementally with type-appropriate defaults.
Args:
col_pos: Column position index
row_index: Row index
col_def: DataGridColumnState for the column
Returns:
list[FormatRule] or None if no formatting defined
col_def: Column definition with title and type already set.
col_id is derived from title via make_safe_id.
"""
col_def.col_id = make_unique_safe_id(col_def.title, [c.col_id for c in self._state.columns])
cell_id = self._get_element_id_from_pos("cell", (col_pos, row_index))
if col_def.type == ColumnType.Formula:
col_def.col_index = -1
self._state.columns.append(col_def)
return
if cell_id in self._state.cell_formats:
return self._state.cell_formats[cell_id]
default_value = column_type_defaults.get(col_def.type, "")
col_def.col_index = len(self._df.columns) if self._df is not None else 0
self._state.columns.append(col_def)
row_state = next((r for r in self._state.rows if r.row_id == row_index), None)
if row_state and row_state.format:
return row_state.format
if col_def.format:
return col_def.format
if self._state.table_format:
return self._state.table_format
# Get global tables formatting from manager
return self._parent.all_tables_formats
if self._df is not None:
self._df_store.ne_df[col_def.col_id] = default_value
self._df_store.ns_fast_access[col_def.col_id] = self._df_store.ne_df[col_def.col_id].to_numpy()
for row_dict in self._df_store.ns_row_data:
row_dict[col_def.col_id] = default_value
self._df_store.save()
def set_column_width(self, col_id: str, width: str):
"""Update column width after resize. Called via Command from JS."""
@@ -609,7 +640,6 @@ class DataGrid(MultipleInstance):
def get_formula_engine(self):
"""Return the FormulaEngine from the DataGridsManager, if available."""
return self._parent.get_formula_engine()
def mk_headers(self):
resize_cmd = self.commands.set_column_width()
@@ -676,7 +706,7 @@ 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
# 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:
@@ -696,7 +726,7 @@ class DataGrid(MultipleInstance):
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)
@@ -740,7 +770,7 @@ class DataGrid(MultipleInstance):
"""
if not col_def.visible:
return None
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)
@@ -969,7 +999,7 @@ class DataGrid(MultipleInstance):
res = []
extra_attr = {
"hx-on::after-settle": f"initDataGridScrollbars('{self._id}');",
"hx-on::after-settle": f"initDataGrid('{self._id}');",
}
if fragment == "body":

View File

@@ -134,6 +134,8 @@ class DataGridColumnsManager(MultipleInstance):
def save_column_details(self, col_id, client_response):
logger.debug(f"save_column_details {col_id=}, {client_response=}")
col_def = self._get_updated_col_def_from_col_id(col_id, client_response, copy=False)
if col_def.col_id == "__new__":
self._parent.add_new_column(col_def) # sets the correct col_id before _register_formula
self._register_formula(col_def)
self._parent.save_state()

View File

@@ -1,3 +1,4 @@
import pandas as pd
from fasthtml.components import *
from myfasthtml.core.bindings import Binding
@@ -151,9 +152,9 @@ icons = {
None: question20_regular,
True: checkbox_checked20_regular,
False: checkbox_unchecked20_regular,
"Brain": brain_circuit20_regular,
ColumnType.RowIndex: number_symbol20_regular,
ColumnType.Text: text_field20_regular,
ColumnType.Number: number_row20_regular,
@@ -162,3 +163,11 @@ icons = {
ColumnType.Enum: text_bullet_list_square20_regular,
ColumnType.Formula: math_formula16_regular,
}
column_type_defaults = {
ColumnType.Number: 0,
ColumnType.Text: "",
ColumnType.Bool: False,
ColumnType.Datetime: pd.NaT,
}

View File

@@ -323,6 +323,18 @@ def make_safe_id(s: str | None):
return res.lower() # no uppercase
def make_unique_safe_id(s: str | None, existing_ids: list):
if s is None:
return None
base = make_safe_id(s)
res = base
i = 1
while res in existing_ids:
res = f"{base}_{i}"
i += 1
return res
def get_class(qualified_class_name: str):
"""
Dynamically loads and returns a class type from its fully qualified name.