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

View File

@@ -3,6 +3,8 @@ import logging
from fasthtml.components import * from fasthtml.components import *
from myfasthtml.controls.BaseCommands import BaseCommands 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.Search import Search
from myfasthtml.controls.datagrid_objects import DataGridColumnState from myfasthtml.controls.datagrid_objects import DataGridColumnState
from myfasthtml.controls.helpers import icons, mk from myfasthtml.controls.helpers import icons, mk
@@ -42,23 +44,71 @@ class Commands(BaseCommands):
self._owner.update_column, self._owner.update_column,
kwargs={"col_id": col_id} kwargs={"col_id": col_id}
).htmx(target=f"#{self._id}", swap="innerHTML") ).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): class DataGridColumnsManager(MultipleInstance):
def __init__(self, parent, _id=None): def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id) super().__init__(parent, _id=_id)
self.commands = Commands(self) 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 @property
def columns(self): def columns(self):
return self._parent.get_state().columns 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] cols_defs = [c for c in self.columns if c.col_id == col_id]
if not cols_defs: if not cols_defs:
return None col_def = DataGridColumnState(col_id, -1)
else: 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): def toggle_column(self, col_id):
logger.debug(f"toggle_column {col_id=}") logger.debug(f"toggle_column {col_id=}")
@@ -81,34 +131,28 @@ class DataGridColumnsManager(MultipleInstance):
return self.mk_column_details(col_def) return self.mk_column_details(col_def)
def show_all_columns(self): def show_all_columns(self):
return self.mk_all_columns() return self._mk_inner_content()
def update_column(self, col_id, client_response): def update_column(self, col_id, client_response):
logger.debug(f"update_column {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: if col_def is None:
logger.debug(f" column '{col_id}' is not found.") logger.debug(f" column '{col_id}' is not found.")
else: else:
for k, v in client_response.items(): # save the new values
if not hasattr(col_def, k): self._parent.save_state()
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()
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: 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."""
@@ -155,6 +199,26 @@ class DataGridColumnsManager(MultipleInstance):
value=col_def.col_id, value=col_def.col_id,
readonly=True), 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(
Div( Div(
Label("Visible"), Label("Visible"),
@@ -173,30 +237,6 @@ class DataGridColumnsManager(MultipleInstance):
cls="flex", 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", legend="Column details",
cls="fieldset border-base-300 rounded-box" cls="fieldset border-base-300 rounded-box"
), ),
@@ -215,9 +255,19 @@ class DataGridColumnsManager(MultipleInstance):
max_height=None 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): def render(self):
return Div( return Div(
self.mk_all_columns(), *self._mk_inner_content(),
id=self._id, id=self._id,
) )

View File

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

View File

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

View File

@@ -21,6 +21,10 @@ class DataGridColumnState:
width: int = DATAGRID_DEFAULT_COLUMN_WIDTH width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
format: list = field(default_factory=list) # format: list = field(default_factory=list) #
formula: str = "" # formula expression for ColumnType.Formula columns 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 @dataclass

View File

@@ -115,6 +115,30 @@ class DbObject:
return props return props
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
"""
Update instance attributes with the provided arguments or keyword arguments.
This method allows updating the attributes of an object based on the provided
dictionary-like argument or explicit keyword arguments. It ensures that only
permitted attributes will be updated, excluding any internal or restricted
attributes. If both a dictionary and keyword arguments are provided, the
properties from the dictionary will be updated first, followed by the
keyword arguments.
There will be only one update in database.
:param args: Zero or one positional argument is allowed. If provided, it must be
either a dictionary or an instance of SimpleNamespace whose properties
are used to update the instance.
:param kwargs: Keyword arguments that represent the properties and their new
values to be updated on the instance.
:return: The updated instance (self).
:rtype: object
:raises ValueError: If more than one positional argument is provided, or if the
provided argument is neither a dictionary nor an instance of
SimpleNamespace.
"""
if len(args) > 1: if len(args) > 1:
raise ValueError("Only one argument is allowed") raise ValueError("Only one argument is allowed")