diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py
index 46d944c..7217df9 100644
--- a/src/myfasthtml/controls/DataGrid.py
+++ b/src/myfasthtml/controls/DataGrid.py
@@ -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)
diff --git a/src/myfasthtml/controls/DataGridColumnsManager.py b/src/myfasthtml/controls/DataGridColumnsManager.py
index 83eba7d..88b2b9f 100644
--- a/src/myfasthtml/controls/DataGridColumnsManager.py
+++ b/src/myfasthtml/controls/DataGridColumnsManager.py
@@ -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,
)
diff --git a/src/myfasthtml/controls/DataGridFormulaEditor.py b/src/myfasthtml/controls/DataGridFormulaEditor.py
index 9bc7744..85ddc53 100644
--- a/src/myfasthtml/controls/DataGridFormulaEditor.py
+++ b/src/myfasthtml/controls/DataGridFormulaEditor.py
@@ -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")
diff --git a/src/myfasthtml/controls/DslEditor.py b/src/myfasthtml/controls/DslEditor.py
index 1579e57..6129e2f 100644
--- a/src/myfasthtml/controls/DslEditor.py
+++ b/src/myfasthtml/controls/DslEditor.py
@@ -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(
diff --git a/src/myfasthtml/controls/datagrid_objects.py b/src/myfasthtml/controls/datagrid_objects.py
index 9354740..2cb7c79 100644
--- a/src/myfasthtml/controls/datagrid_objects.py
+++ b/src/myfasthtml/controls/datagrid_objects.py
@@ -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
diff --git a/src/myfasthtml/core/dbmanager.py b/src/myfasthtml/core/dbmanager.py
index ebdcaa7..e52d0e7 100644
--- a/src/myfasthtml/core/dbmanager.py
+++ b/src/myfasthtml/core/dbmanager.py
@@ -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")