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")