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
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -43,22 +45,70 @@ class Commands(BaseCommands):
|
|||||||
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":
|
return self._mk_inner_content()
|
||||||
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
|
def on_new_column(self):
|
||||||
self._parent.save_state()
|
self._new_column = True
|
||||||
|
col_def = self._get_col_def_from_col_id("__new__")
|
||||||
|
return self.mk_column_details(col_def)
|
||||||
|
|
||||||
return self.mk_all_columns()
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class DataGridColumnState:
|
|||||||
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
|
||||||
class DatagridEditionState:
|
class DatagridEditionState:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user