Integrating formula editor

This commit is contained in:
2026-02-13 23:04:06 +01:00
parent e8443f07f9
commit 789c06b842
6 changed files with 171 additions and 91 deletions

View File

@@ -430,7 +430,7 @@ class DataGrid(MultipleInstance):
Called after data reload to ensure the engine knows about all
formula columns and their expressions.
"""
engine = self._get_formula_engine()
engine = self.get_formula_engine()
if engine is None:
return
table = self.get_table_name()
@@ -448,7 +448,7 @@ class DataGrid(MultipleInstance):
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()
engine = self.get_formula_engine()
if engine is None:
return
engine.recalculate_if_needed(self.get_table_name(), self._df_store)

View File

@@ -3,6 +3,8 @@ import logging
from fasthtml.components import *
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.DataGridFormulaEditor import DataGridFormulaEditor
from myfasthtml.controls.DslEditor import DslEditorConf
from myfasthtml.controls.Search import Search
from myfasthtml.controls.datagrid_objects import DataGridColumnState
from myfasthtml.controls.helpers import icons, mk
@@ -42,23 +44,71 @@ class Commands(BaseCommands):
self._owner.update_column,
kwargs={"col_id": col_id}
).htmx(target=f"#{self._id}", swap="innerHTML")
def on_new_column(self):
return Command(f"OnNewColumn",
f"New column",
self._owner,
self._owner.on_new_column).htmx(target=f"#{self._id}", swap="innerHTML")
def on_column_type_changed(self):
return Command(f"OnColumnTypeChanged",
f"Column Type changed",
self._owner,
self._owner.on_column_type_changed).htmx(target=f"#{self._id}", swap="innerHTML", trigger="change")
class DataGridColumnsManager(MultipleInstance):
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id)
self.commands = Commands(self)
self._new_column = False
conf = DslEditorConf(save_button=False, placeholder="{Column} * {OtherColumn}", line_numbers=False)
self._formula_editor = DataGridFormulaEditor(self, conf=conf, _id=f"{self._id}-formula-editor")
@property
def columns(self):
return self._parent.get_state().columns
def _get_col_def_from_col_id(self, col_id):
def _get_col_def_from_col_id(self, col_id, updates=None, copy=True):
"""
Retrieves a column definition from the column ID, with optional updates
made using information provided by the client response.
:param col_id: The unique identifier of the column to retrieve.
:param updates: Optional dictionary containing updated values for
the column attributes provided by the client. If specified, the
attributes of the column definition will be updated accordingly.
:return: A copy of the column definition object with any updates applied,
or None if no column matching the provided ID is found.
:rtype: ColumnDefinition | None
"""
if updates:
updates["visible"] = "visible" in updates and updates["visible"] == "on"
cols_defs = [c for c in self.columns if c.col_id == col_id]
if not cols_defs:
return None
col_def = DataGridColumnState(col_id, -1)
else:
return cols_defs[0]
col_def = cols_defs[0].copy() if copy else cols_defs[0]
if updates:
for k, v in [(k, v) for k, v in updates.items() if hasattr(col_def, k)]:
if k == "visible":
col_def.visible = v == "on"
elif k == "type":
col_def.type = ColumnType(v)
elif k == "width":
col_def.width = int(v)
elif k == "formula":
col_def.formula = v or ""
self._register_formula(col_def)
else:
setattr(col_def, k, v)
return col_def
def toggle_column(self, col_id):
logger.debug(f"toggle_column {col_id=}")
@@ -81,34 +131,28 @@ class DataGridColumnsManager(MultipleInstance):
return self.mk_column_details(col_def)
def show_all_columns(self):
return self.mk_all_columns()
return self._mk_inner_content()
def update_column(self, col_id, client_response):
logger.debug(f"update_column {col_id=}, {client_response=}")
col_def = self._get_col_def_from_col_id(col_id)
col_def = self._get_col_def_from_col_id(col_id, client_response, copy=False)
if col_def is None:
logger.debug(f" column '{col_id}' is not found.")
else:
for k, v in client_response.items():
if not hasattr(col_def, k):
continue
if k == "visible":
col_def.visible = v == "on"
elif k == "type":
col_def.type = ColumnType(v)
elif k == "width":
col_def.width = int(v)
elif k == "formula":
col_def.formula = v or ""
self._register_formula(col_def)
else:
setattr(col_def, k, v)
# save the new values
self._parent.save_state()
# save the new values
self._parent.save_state()
return self.mk_all_columns()
return self._mk_inner_content()
def on_new_column(self):
self._new_column = True
col_def = self._get_col_def_from_col_id("__new__")
return self.mk_column_details(col_def)
def on_column_type_changed(self, col_id, client_response):
logger.debug(f"on_column_type_changed {col_id=}, {client_response=}")
col_def = self._get_col_def_from_col_id(col_id, client_response)
return self.mk_column_details(col_def)
def _register_formula(self, col_def) -> None:
"""Register or remove a formula column with the FormulaEngine."""
@@ -155,6 +199,26 @@ class DataGridColumnsManager(MultipleInstance):
value=col_def.col_id,
readonly=True),
Label("Title"),
Input(name="title",
cls=f"input input-{size}",
value=col_def.title),
Label("type"),
mk.mk(
Select(
*[Option(option.value, value=option.value, selected=option == col_def.type) for option in ColumnType],
name="type",
cls=f"select select-{size}",
value=col_def.title,
), command=self.commands.on_column_type_changed()
),
*([
Label("Formula"),
self._formula_editor,
] if col_def.type == ColumnType.Formula else []),
Div(
Div(
Label("Visible"),
@@ -173,30 +237,6 @@ class DataGridColumnsManager(MultipleInstance):
cls="flex",
),
Label("Title"),
Input(name="title",
cls=f"input input-{size}",
value=col_def.title),
Label("type"),
Select(
*[Option(option.value, value=option.value, selected=option == col_def.type) for option in ColumnType],
name="type",
cls=f"select select-{size}",
value=col_def.title,
),
*([
Label("Formula"),
Textarea(
col_def.formula or "",
name="formula",
cls=f"textarea textarea-{size} w-full font-mono",
placeholder="{Column} * {OtherColumn}",
rows=3,
),
] if col_def.type == ColumnType.Formula else []),
legend="Column details",
cls="fieldset border-base-300 rounded-box"
),
@@ -215,9 +255,19 @@ class DataGridColumnsManager(MultipleInstance):
max_height=None
)
def mk_new_column(self):
return Div(
mk.button("New Column", command=self.commands.on_new_column()),
cls="mb-1",
)
def _mk_inner_content(self):
return (self.mk_all_columns(),
self.mk_new_column())
def render(self):
return Div(
self.mk_all_columns(),
*self._mk_inner_content(),
id=self._id,
)

View File

@@ -9,7 +9,7 @@ Extends DslEditor with formula-specific behavior:
import logging
from myfasthtml.controls.DslEditor import DslEditor
from myfasthtml.core.formula.dsl.exceptions import FormulaSyntaxError, FormulaCycleError
from myfasthtml.core.formula.dsl.definition import FormulaDSL
logger = logging.getLogger("DataGridFormulaEditor")
@@ -25,43 +25,42 @@ class DataGridFormulaEditor(DslEditor):
_id: Optional instance ID.
"""
def __init__(self, parent, col_def, conf=None, _id=None):
super().__init__(parent, conf=conf, _id=_id)
self._col_def = col_def
def __init__(self, parent, conf=None, _id=None):
super().__init__(parent, FormulaDSL(), conf=conf, _id=_id)
def on_content_changed(self):
"""
Called when the formula text is changed in the editor.
1. Updates col_def.formula with the new text.
2. Registers the formula with the FormulaEngine.
3. Triggers a body re-render of the parent DataGrid.
"""
formula_text = self.get_content()
# Update the column definition
self._col_def.formula = formula_text or ""
# Register with the FormulaEngine
engine = self._parent._get_formula_engine()
if engine is not None:
table = self._parent.get_table_name()
try:
engine.set_formula(table, self._col_def.col_id, formula_text)
logger.debug(
"Formula updated for %s.%s: %s",
table, self._col_def.col_id, formula_text,
)
except FormulaSyntaxError as e:
logger.debug("Formula syntax error, keeping old formula: %s", e)
return
except FormulaCycleError as e:
logger.warning("Formula cycle detected for %s.%s: %s", table, self._col_def.col_id, e)
return
except Exception as e:
logger.warning("Formula engine error for %s.%s: %s", table, self._col_def.col_id, e)
return
# Save state and re-render the grid body
self._parent.save_state()
return self._parent.render_partial("body")
# def on_content_changed(self):
# """
# Called when the formula text is changed in the editor.
#
# 1. Updates col_def.formula with the new text.
# 2. Registers the formula with the FormulaEngine.
# 3. Triggers a body re-render of the parent DataGrid.
# """
# formula_text = self.get_content()
#
# # Update the column definition
# self._col_def.formula = formula_text or ""
#
# # Register with the FormulaEngine
# engine = self._parent.get_formula_engine()
# if engine is not None:
# table = self._parent.get_table_name()
# try:
# engine.set_formula(table, self._col_def.col_id, formula_text)
# logger.debug(
# "Formula updated for %s.%s: %s",
# table, self._col_def.col_id, formula_text,
# )
# except FormulaSyntaxError as e:
# logger.debug("Formula syntax error, keeping old formula: %s", e)
# return
# except FormulaCycleError as e:
# logger.warning("Formula cycle detected for %s.%s: %s", table, self._col_def.col_id, e)
# return
# except Exception as e:
# logger.warning("Formula engine error for %s.%s: %s", table, self._col_def.col_id, e)
# return
#
# # Save state and re-render the grid body
# self._parent.save_state()
# return self._parent.render_partial("body")

View File

@@ -33,6 +33,7 @@ class DslEditorConf:
placeholder: str = ""
readonly: bool = False
engine_id: str = None # id of the DSL engine to use for autocompletion
save_button: bool = True
class DslEditorState(DbObject):
@@ -184,6 +185,8 @@ class DslEditor(MultipleInstance):
return Script(f"initDslEditor({config_json});")
def _mk_auto_save(self):
if not self.conf.save_button:
return None
return Div(
Label(
mk.mk(

View File

@@ -21,6 +21,10 @@ class DataGridColumnState:
width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
format: list = field(default_factory=list) #
formula: str = "" # formula expression for ColumnType.Formula columns
def copy(self):
props = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
return DataGridColumnState(**props)
@dataclass