diff --git a/src/myfasthtml/assets/core/myfasthtml.js b/src/myfasthtml/assets/core/myfasthtml.js index ae3ca8f..5b1057a 100644 --- a/src/myfasthtml/assets/core/myfasthtml.js +++ b/src/myfasthtml/assets/core/myfasthtml.js @@ -375,7 +375,7 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) { } // Make AJAX call with htmx - console.debug(`Triggering HTMX action for element ${elementId}: ${method} ${url}`, htmxOptions); + //console.debug(`Triggering HTMX action for element ${elementId}: ${method} ${url}`, htmxOptions); htmx.ajax(method, url, htmxOptions); } diff --git a/src/myfasthtml/controls/CycleStateControl.py b/src/myfasthtml/controls/CycleStateControl.py index ebb8ad9..9b22187 100644 --- a/src/myfasthtml/controls/CycleStateControl.py +++ b/src/myfasthtml/controls/CycleStateControl.py @@ -1,3 +1,5 @@ +import logging + from fasthtml.components import Div from myfasthtml.controls.BaseCommands import BaseCommands @@ -6,6 +8,7 @@ from myfasthtml.core.commands import Command from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.instances import MultipleInstance +logger = logging.getLogger("CycleStateControl") class CycleState(DbObject): def __init__(self, owner, save_state): @@ -24,7 +27,7 @@ class Commands(BaseCommands): class CycleStateControl(MultipleInstance): def __init__(self, parent, controls: dict, _id=None, save_state=True): - super().__init__(parent, _id) + super().__init__(parent, _id=_id) self._state = CycleState(self, save_state) self.controls_by_states = controls self.commands = Commands(self) @@ -34,6 +37,7 @@ class CycleStateControl(MultipleInstance): self._state.state = next(iter(controls.keys())) def cycle_state(self): + logger.debug(f"cycle_state datagrid={self._parent.get_table_name()}") keys = list(self.controls_by_states.keys()) current_idx = keys.index(self._state.state) self._state.state = keys[(current_idx + 1) % len(keys)] diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index 9cc68c3..3ab6893 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -203,7 +203,10 @@ class DataGrid(MultipleInstance): "column": mk.icon(column, tooltip="Column selection"), "cell": mk.icon(grid, tooltip="Cell selection") } - self._selection_mode_selector = CycleStateControl(self, controls=selection_types, save_state=False) + self._selection_mode_selector = CycleStateControl(self, + controls=selection_types, + save_state=False, + _id="-cycle_state") self._selection_mode_selector.bind_command("CycleState", self.commands.change_selection_mode()) # add columns manager diff --git a/src/myfasthtml/controls/DataGridsManager.py b/src/myfasthtml/controls/DataGridsManager.py index c0492ab..7dc67d9 100644 --- a/src/myfasthtml/controls/DataGridsManager.py +++ b/src/myfasthtml/controls/DataGridsManager.py @@ -167,9 +167,12 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider): def list_column_values(self, table_name, column_name): return self._registry.get_column_values(table_name, column_name) - + def get_row_count(self, table_name): return self._registry.get_row_count(table_name) + + def get_column_type(self, table_name, column_name): + return self._registry.get_column_type(table_name, column_name) def list_style_presets(self) -> list[str]: return list(self.style_presets.keys()) diff --git a/src/myfasthtml/core/DataGridsRegistry.py b/src/myfasthtml/core/DataGridsRegistry.py index 255c430..d7f408c 100644 --- a/src/myfasthtml/core/DataGridsRegistry.py +++ b/src/myfasthtml/core/DataGridsRegistry.py @@ -59,15 +59,42 @@ class DataGridsRegistry(SingleInstance): try: as_fullname_dict = self._get_entries_as_full_name_dict() grid_id = as_fullname_dict[table_name] - + # load dataframe state_id = f"{grid_id}#state" state = self._db_manager.load(state_id) df = state["ne_df"] if state else None return len(df) if df is not None else 0 - + except KeyError: return 0 + + def get_column_type(self, table_name, column_name): + """ + Get the type of a column. + + Args: + table_name: The DataGrid name + column_name: The column name + + Returns: + ColumnType enum value or None if not found + """ + try: + as_fullname_dict = self._get_entries_as_full_name_dict() + grid_id = as_fullname_dict[table_name] + + # load datagrid state + state_id = f"{grid_id}#state" + state = self._db_manager.load(state_id) + + if state and "columns" in state: + for col in state["columns"]: + if col.col_id == column_name: + return col.type + return None + except KeyError: + return None def _get_all_entries(self): return {k: v for k, v in self._db_manager.load(DATAGRIDS_REGISTRY_ENTRY_KEY).items() diff --git a/src/myfasthtml/core/commands.py b/src/myfasthtml/core/commands.py index fec03bb..a69ddc0 100644 --- a/src/myfasthtml/core/commands.py +++ b/src/myfasthtml/core/commands.py @@ -48,10 +48,13 @@ class Command: # either there is no parameter (so one single instance of the command is enough) # or the parameter is a kwargs (so the parameters are provided when the command is called) if key is None: - if owner is not None and args is None: # args is not provided - key = f"{owner.get_full_id()}-{name}" - else: - key = f"{name}-{_compute_from_args()}" + key_parts = [] + if owner is not None: + key_parts.append(f"{owner.get_full_id()}") + key_parts.append(name) + if args: + key_parts.append(_compute_from_args()) + key = "-".join(key_parts) else: key = key.replace("#{args}", _compute_from_args()) if owner is not None: @@ -122,6 +125,8 @@ class Command: def execute(self, client_response: dict = None): logger.debug(f"Executing command {self.name} with arguments {client_response=}") + if self._htmx_extra.get("hx-target", "").startswith("#tsm_"): + logger.warning(f" Command {self.name} needs a selection manager to work properly.") with ObservableResultCollector(self._bindings) as collector: kwargs = self._create_kwargs(self.default_kwargs, client_response, @@ -145,6 +150,10 @@ class Command: and r.get("id", None) is not None): r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true") + if self._htmx_extra.get("hx-target", "").startswith("#tsm_"): + ret_debug = [f"<{r.tag} id={r.attrs.get('id', '')}/>" if r else "None" for r in all_ret] + logger.warning(f" {ret_debug=}") + return all_ret[0] if len(all_ret) == 1 else all_ret def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None, auto_swap_oob=True): diff --git a/src/myfasthtml/core/dbmanager.py b/src/myfasthtml/core/dbmanager.py index 76a79d7..ebdcaa7 100644 --- a/src/myfasthtml/core/dbmanager.py +++ b/src/myfasthtml/core/dbmanager.py @@ -79,11 +79,10 @@ class DbObject: return # still under initialization if self._reload_self(): - logger.debug(f"finalize_initialization ({self._name}) : Loaded existing content.") + # logger.debug(f"finalize_initialization ({self._name}) : Loaded existing content.") return else: - logger.debug( - f"finalize_initialization ({self._name}) : No existing content found, creating new entry {self._save_state=}.") + # logger.debug(f"finalize_initialization ({self._name}) : No existing content found, creating new entry {self._save_state=}.") self._save_self() def _reload_self(self): diff --git a/src/myfasthtml/core/formatting/dsl/completion/FormattingCompletionEngine.py b/src/myfasthtml/core/formatting/dsl/completion/FormattingCompletionEngine.py index 4457905..9582d88 100644 --- a/src/myfasthtml/core/formatting/dsl/completion/FormattingCompletionEngine.py +++ b/src/myfasthtml/core/formatting/dsl/completion/FormattingCompletionEngine.py @@ -181,8 +181,8 @@ class FormattingCompletionEngine(BaseCompletionEngine): return presets.COMPARISON_OPERATORS case Context.OPERATOR_VALUE | Context.BETWEEN_VALUE: - # col., True, False + column values - base = presets.OPERATOR_VALUE_BASE.copy() + # Filter base suggestions according to column type + base = self._get_base_suggestions_for_column_type(scope) base.extend(self._get_column_value_suggestions(scope)) return base @@ -363,6 +363,44 @@ class FormattingCompletionEngine(BaseCompletionEngine): except Exception: return [] + def _get_base_suggestions_for_column_type(self, scope: DetectedScope) -> list[Suggestion]: + """ + Filter base suggestions according to column type. + + Only suggests True/False for Boolean columns. + Always suggests col. for cross-column comparisons. + + Args: + scope: The detected scope containing column information + + Returns: + List of filtered base suggestions + """ + from myfasthtml.core.constants import ColumnType + + if not scope.column_name: + # No column context, return all base suggestions + return presets.OPERATOR_VALUE_BASE.copy() + + try: + table_name = scope.table_name or self.table_name or "" + col_type = self.provider.get_column_type(table_name, scope.column_name) + + suggestions = [] + + # Always suggest col. for cross-column comparisons + suggestions.append(Suggestion("col.", "Reference another column", "keyword")) + + # Add True/False only for Boolean columns + if col_type == ColumnType.Bool: + suggestions.append(Suggestion("True", "Boolean true", "literal")) + suggestions.append(Suggestion("False", "Boolean false", "literal")) + + return suggestions + except Exception: + # Fallback to default if type detection fails + return presets.OPERATOR_VALUE_BASE.copy() + def get_completions( text: str, diff --git a/src/myfasthtml/core/formatting/dsl/completion/provider.py b/src/myfasthtml/core/formatting/dsl/completion/provider.py index a1592e3..b48fef7 100644 --- a/src/myfasthtml/core/formatting/dsl/completion/provider.py +++ b/src/myfasthtml/core/formatting/dsl/completion/provider.py @@ -72,3 +72,18 @@ class DatagridMetadataProvider(BaseMetadataProvider): Number of rows """ ... + + def get_column_type(self, table_name: str, column_name: str): + """ + Return the type of a column. + + Used to filter suggestions based on column type. + + Args: + table_name: The DataGrid name + column_name: The column name + + Returns: + ColumnType enum value or None if not found + """ + ... diff --git a/src/myfasthtml/core/instances.py b/src/myfasthtml/core/instances.py index 2c66d0f..662a974 100644 --- a/src/myfasthtml/core/instances.py +++ b/src/myfasthtml/core/instances.py @@ -6,6 +6,8 @@ from myfasthtml.controls.helpers import Ids from myfasthtml.core.constants import NO_DEFAULT_VALUE from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal +VERBOSE_VERBOSE = False + logger = logging.getLogger("InstancesManager") special_session = { @@ -26,12 +28,19 @@ class BaseInstance: def __new__(cls, *args, **kwargs): # Extract arguments from both positional and keyword arguments # Signature matches __init__: parent, session=None, _id=None, auto_register=True + if VERBOSE_VERBOSE: + logger.debug(f"Creating new instance of type {cls.__name__}") + parent = args[0] if len(args) > 0 and isinstance(args[0], BaseInstance) else kwargs.get("parent", None) session = args[1] if len(args) > 1 and isinstance(args[1], dict) else kwargs.get("session", None) _id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None) + if VERBOSE_VERBOSE: + logger.debug(f" parent={parent}, session={session}, _id={_id}") # Compute _id _id = cls.compute_id(_id, parent) + if VERBOSE_VERBOSE: + logger.debug(f" computed id={_id}") if session is None: if parent is not None: @@ -46,9 +55,14 @@ class BaseInstance: res = InstancesManager.instances[key] if type(res) is not cls: raise TypeError(f"Instance with id {_id} already exists, but is of type {type(res)}") + + if VERBOSE_VERBOSE: + logger.debug(f" instance {_id} already exists, returning existing instance") return res # Otherwise create a new instance + if VERBOSE_VERBOSE: + logger.debug(f" creating new instance") instance = super().__new__(cls) instance._is_new_instance = True # mark as fresh return instance