|
|
|
|
@@ -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":
|
|
|
|
|
|