Integrating formula editor
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -43,22 +45,70 @@ class Commands(BaseCommands):
|
||||
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()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -22,6 +22,10 @@ class DataGridColumnState:
|
||||
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
|
||||
class DatagridEditionState:
|
||||
|
||||
@@ -115,6 +115,30 @@ class DbObject:
|
||||
return props
|
||||
|
||||
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:
|
||||
raise ValueError("Only one argument is allowed")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user