I can format columns
This commit is contained in:
@@ -1403,6 +1403,8 @@ These warnings would be non-blocking (DSL still parses) but displayed with a dif
|
|||||||
3. ~~**Translate lark grammar to Lezer**~~ Done (`lark_to_lezer.py`)
|
3. ~~**Translate lark grammar to Lezer**~~ Done (`lark_to_lezer.py`)
|
||||||
4. ~~**Build CodeMirror extension**~~ Done (`DslEditor.py` + `initDslEditor()`)
|
4. ~~**Build CodeMirror extension**~~ Done (`DslEditor.py` + `initDslEditor()`)
|
||||||
5. ~~**Integrate with DataGrid** - connect DSL output to formatting engine~~ Done
|
5. ~~**Integrate with DataGrid** - connect DSL output to formatting engine~~ Done
|
||||||
|
- `DataGridFormattingEditor.on_content_changed()` dispatches rules to column/row/cell formats
|
||||||
|
- `DataGrid.mk_body_cell_content()` applies formatting via `FormattingEngine`
|
||||||
6. ~~**Implement dynamic client-side completions**~~ Done
|
6. ~~**Implement dynamic client-side completions**~~ Done
|
||||||
- Engine ID: `DSLDefinition.get_id()` passed via `completionEngineId` in JS config
|
- Engine ID: `DSLDefinition.get_id()` passed via `completionEngineId` in JS config
|
||||||
- Trigger: Hybrid (Ctrl+Space + auto after `.` `(` `"` and space)
|
- Trigger: Hybrid (Ctrl+Space + auto after `.` `(` `"` and space)
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
| `row` parameter (column-level conditions) | :x: Not implemented | |
|
| `row` parameter (column-level conditions) | :x: Not implemented | |
|
||||||
| Column reference in value `{"col": "..."}` | :white_check_mark: Implemented | |
|
| Column reference in value `{"col": "..."}` | :white_check_mark: Implemented | |
|
||||||
| **DataGrid Integration** | | |
|
| **DataGrid Integration** | | |
|
||||||
| Integration in `mk_body_cell_content()` | :x: Not implemented | |
|
| Integration in `mk_body_cell_content()` | :white_check_mark: Implemented | `DataGrid.py` |
|
||||||
|
| DataGridFormattingEditor | :white_check_mark: Implemented | `DataGridFormattingEditor.py` |
|
||||||
| DataGridsManager (global presets) | :white_check_mark: Implemented | `DataGridsManager.py` |
|
| DataGridsManager (global presets) | :white_check_mark: Implemented | `DataGridsManager.py` |
|
||||||
| **Tests** | | `tests/core/formatting/` |
|
| **Tests** | | `tests/core/formatting/` |
|
||||||
| test_condition_evaluator.py | :white_check_mark: ~45 test cases | |
|
| test_condition_evaluator.py | :white_check_mark: ~45 test cases | |
|
||||||
@@ -537,4 +538,4 @@ All items below are :x: **not implemented**.
|
|||||||
- **API source for enum**: `{"type": "api", "value": "https://...", ...}`
|
- **API source for enum**: `{"type": "api", "value": "https://...", ...}`
|
||||||
- **Searchable enum**: For large option lists
|
- **Searchable enum**: For large option lists
|
||||||
- **Formatter chaining**: Apply multiple formatters in sequence
|
- **Formatter chaining**: Apply multiple formatters in sequence
|
||||||
- **DataGrid integration**: Connect `FormattingEngine` to `DataGrid.mk_body_cell_content()`
|
- ~~**DataGrid integration**: Connect `FormattingEngine` to `DataGrid.mk_body_cell_content()`~~ Done
|
||||||
|
|||||||
@@ -1000,7 +1000,7 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) {
|
|||||||
|
|
||||||
for (const [combinationStr, config] of Object.entries(combinations)) {
|
for (const [combinationStr, config] of Object.entries(combinations)) {
|
||||||
const sequence = parseCombination(combinationStr);
|
const sequence = parseCombination(combinationStr);
|
||||||
console.log("Parsing mouse combination", combinationStr, "=>", sequence);
|
//console.log("Parsing mouse combination", combinationStr, "=>", sequence);
|
||||||
let currentNode = root;
|
let currentNode = root;
|
||||||
|
|
||||||
for (const actionSet of sequence) {
|
for (const actionSet of sequence) {
|
||||||
@@ -1137,7 +1137,7 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) {
|
|||||||
const clickStart = performance.now();
|
const clickStart = performance.now();
|
||||||
const elementCount = MouseRegistry.elements.size;
|
const elementCount = MouseRegistry.elements.size;
|
||||||
|
|
||||||
console.warn(`🖱️ Click handler START: processing ${elementCount} registered elements`);
|
//console.warn(`🖱️ Click handler START: processing ${elementCount} registered elements`);
|
||||||
|
|
||||||
// Create a snapshot of current mouse action with modifiers
|
// Create a snapshot of current mouse action with modifiers
|
||||||
const snapshot = createSnapshot(event, 'click');
|
const snapshot = createSnapshot(event, 'click');
|
||||||
@@ -2358,7 +2358,7 @@ function initDslEditor(config) {
|
|||||||
setContent: (content) => editor.setValue(content)
|
setContent: (content) => editor.setValue(content)
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug(`DslEditor initialized: ${elementId}, DSL=${dsl?.name || "unknown"}, dsl_id=${dslId}, completion=${enableCompletion ? "enabled" : "disabled"}, linting=${enableLinting ? "enabled" : "disabled"}`);
|
//console.debug(`DslEditor initialized: ${elementId}, DSL=${dsl?.name || "unknown"}, dsl_id=${dslId}, completion=${enableCompletion ? "enabled" : "disabled"}, linting=${enableLinting ? "enabled" : "disabled"}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ from myfasthtml.controls.helpers import mk, icons
|
|||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
from myfasthtml.core.formatting.dataclasses import FormatRule, Style, Condition, ConstantFormatter
|
|
||||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||||
from myfasthtml.core.instances import MultipleInstance
|
from myfasthtml.core.instances import MultipleInstance
|
||||||
@@ -181,7 +180,7 @@ class DataGrid(MultipleInstance):
|
|||||||
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
||||||
|
|
||||||
# add Panel
|
# add Panel
|
||||||
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="#panel")
|
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
|
||||||
self._panel.set_side_visible("right", False) # the right Panel always starts closed
|
self._panel.set_side_visible("right", False) # the right Panel always starts closed
|
||||||
self.bind_command("ToggleColumnsManager", self._panel.commands.toggle_side("right"))
|
self.bind_command("ToggleColumnsManager", self._panel.commands.toggle_side("right"))
|
||||||
self.bind_command("ToggleFormattingEditor", self._panel.commands.toggle_side("right"))
|
self.bind_command("ToggleFormattingEditor", self._panel.commands.toggle_side("right"))
|
||||||
@@ -212,7 +211,7 @@ class DataGrid(MultipleInstance):
|
|||||||
conf=editor_conf,
|
conf=editor_conf,
|
||||||
dsl=FormattingDSL(),
|
dsl=FormattingDSL(),
|
||||||
save_state=self._settings.save_state,
|
save_state=self._settings.save_state,
|
||||||
_id="#formatting_editor")
|
_id="-formatting_editor")
|
||||||
|
|
||||||
# other definitions
|
# other definitions
|
||||||
self._mouse_support = {
|
self._mouse_support = {
|
||||||
@@ -398,17 +397,6 @@ class DataGrid(MultipleInstance):
|
|||||||
list[FormatRule] or None if no formatting defined
|
list[FormatRule] or None if no formatting defined
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# hack to test
|
|
||||||
if col_def.col_id == "age":
|
|
||||||
return [
|
|
||||||
FormatRule(style=Style(color="green")),
|
|
||||||
FormatRule(condition=Condition(operator=">", value=18), style=Style(color="red")),
|
|
||||||
]
|
|
||||||
|
|
||||||
return [
|
|
||||||
FormatRule(condition=Condition(operator="isnan"), formatter=ConstantFormatter(value="-")),
|
|
||||||
]
|
|
||||||
|
|
||||||
cell_id = self._get_element_id_from_pos("cell", (col_pos, row_index))
|
cell_id = self._get_element_id_from_pos("cell", (col_pos, row_index))
|
||||||
|
|
||||||
if cell_id in self._state.cell_formats:
|
if cell_id in self._state.cell_formats:
|
||||||
@@ -816,7 +804,7 @@ class DataGrid(MultipleInstance):
|
|||||||
cls="flex items-center justify-between mb-2"),
|
cls="flex items-center justify-between mb-2"),
|
||||||
self._panel.set_main(self.mk_table_wrapper()),
|
self._panel.set_main(self.mk_table_wrapper()),
|
||||||
Script(f"initDataGrid('{self._id}');"),
|
Script(f"initDataGrid('{self._id}');"),
|
||||||
Mouse(self, combinations=self._mouse_support),
|
Mouse(self, combinations=self._mouse_support, _id="-mouse"),
|
||||||
id=self._id,
|
id=self._id,
|
||||||
cls="grid",
|
cls="grid",
|
||||||
style="height: 100%; grid-template-rows: auto 1fr;"
|
style="height: 100%; grid-template-rows: auto 1fr;"
|
||||||
|
|||||||
@@ -1,7 +1,112 @@
|
|||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from myfasthtml.controls.DslEditor import DslEditor
|
from myfasthtml.controls.DslEditor import DslEditor
|
||||||
|
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope
|
||||||
|
|
||||||
|
logger = logging.getLogger("DataGridFormattingEditor")
|
||||||
|
|
||||||
|
|
||||||
class DataGridFormattingEditor(DslEditor):
|
class DataGridFormattingEditor(DslEditor):
|
||||||
|
|
||||||
def on_dsl_change(self, dsl):
|
def _find_column_by_name(self, name: str):
|
||||||
pass
|
"""
|
||||||
|
Find a column by name, searching col_id first, then title.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple (col_pos, col_def) if found, (None, None) otherwise
|
||||||
|
"""
|
||||||
|
# First pass: match by col_id
|
||||||
|
for col_pos, col_def in enumerate(self._parent.get_state().columns):
|
||||||
|
if col_def.col_id == name:
|
||||||
|
return col_pos, col_def
|
||||||
|
|
||||||
|
# Second pass: match by title
|
||||||
|
for col_pos, col_def in enumerate(self._parent.get_state().columns):
|
||||||
|
if col_def.title == name:
|
||||||
|
return col_pos, col_def
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def _get_cell_id(self, scope: CellScope):
|
||||||
|
"""
|
||||||
|
Get cell_id from CellScope.
|
||||||
|
|
||||||
|
If scope has cell_id, use it directly.
|
||||||
|
Otherwise, resolve coordinates (column, row) to cell_id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
cell_id string or None if column not found
|
||||||
|
"""
|
||||||
|
if scope.cell_id:
|
||||||
|
return scope.cell_id
|
||||||
|
|
||||||
|
col_pos, _ = self._find_column_by_name(scope.column)
|
||||||
|
if col_pos is None:
|
||||||
|
logger.warning(f"Column '{scope.column}' not found for CellScope")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._parent._get_element_id_from_pos("cell", (col_pos, scope.row))
|
||||||
|
|
||||||
|
def on_content_changed(self):
|
||||||
|
dsl = self.get_content()
|
||||||
|
|
||||||
|
# Step 1: Parse DSL
|
||||||
|
try:
|
||||||
|
scoped_rules = parse_dsl(dsl)
|
||||||
|
except DSLSyntaxError as e:
|
||||||
|
logger.debug(f"DSL syntax error, keeping old formatting: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 2: Group rules by scope
|
||||||
|
columns_rules = defaultdict(list) # key = column name
|
||||||
|
rows_rules = defaultdict(list) # key = row index
|
||||||
|
cells_rules = defaultdict(list) # key = cell_id
|
||||||
|
|
||||||
|
for scoped_rule in scoped_rules:
|
||||||
|
scope = scoped_rule.scope
|
||||||
|
rule = scoped_rule.rule
|
||||||
|
|
||||||
|
if isinstance(scope, ColumnScope):
|
||||||
|
columns_rules[scope.column].append(rule)
|
||||||
|
elif isinstance(scope, RowScope):
|
||||||
|
rows_rules[scope.row].append(rule)
|
||||||
|
elif isinstance(scope, CellScope):
|
||||||
|
cell_id = self._get_cell_id(scope)
|
||||||
|
if cell_id:
|
||||||
|
cells_rules[cell_id].append(rule)
|
||||||
|
|
||||||
|
# Step 3: Copy state for atomic update
|
||||||
|
state = self._parent.get_state().copy()
|
||||||
|
|
||||||
|
# Step 4: Clear existing formats on the copy
|
||||||
|
for col in state.columns:
|
||||||
|
col.format = None
|
||||||
|
for row in state.rows:
|
||||||
|
row.format = None
|
||||||
|
state.cell_formats.clear()
|
||||||
|
|
||||||
|
# Step 5: Apply grouped rules on the copy
|
||||||
|
for column_name, rules in columns_rules.items():
|
||||||
|
col_pos, col_def = self._find_column_by_name(column_name)
|
||||||
|
if col_def:
|
||||||
|
# Find the column in the copied state
|
||||||
|
state.columns[col_pos].format = rules
|
||||||
|
else:
|
||||||
|
logger.warning(f"Column '{column_name}' not found, skipping rules")
|
||||||
|
|
||||||
|
for row_index, rules in rows_rules.items():
|
||||||
|
if row_index < len(state.rows):
|
||||||
|
state.rows[row_index].format = rules
|
||||||
|
else:
|
||||||
|
logger.warning(f"Row {row_index} out of range, skipping rules")
|
||||||
|
|
||||||
|
for cell_id, rules in cells_rules.items():
|
||||||
|
state.cell_formats[cell_id] = rules
|
||||||
|
|
||||||
|
# Step 6: Update state atomically
|
||||||
|
self._parent.get_state().update(state)
|
||||||
|
|
||||||
|
# Step 7: Refresh the DataGrid
|
||||||
|
logger.debug(f"Formatting applied: {len(columns_rules)} columns, {len(rows_rules)} rows, {len(cells_rules)} cells")
|
||||||
|
return self._parent.render_partial("body")
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ class Commands(BaseCommands):
|
|||||||
self._owner,
|
self._owner,
|
||||||
self._owner.toggle_auto_save).htmx(target=f"#as_{self._id}", trigger="click")
|
self._owner.toggle_auto_save).htmx(target=f"#as_{self._id}", trigger="click")
|
||||||
|
|
||||||
def on_content_changed(self):
|
def save_content(self):
|
||||||
return Command("OnContentChanged",
|
return Command("SaveContent",
|
||||||
"On content changed",
|
"Save content",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_content_changed
|
self._owner.save_content
|
||||||
).htmx(target=None)
|
).htmx(target=None)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,15 +112,24 @@ class DslEditor(MultipleInstance):
|
|||||||
"""Get the current editor content."""
|
"""Get the current editor content."""
|
||||||
return self._state.content
|
return self._state.content
|
||||||
|
|
||||||
def update_content(self, content: str = "") -> None:
|
def update_content(self, content: str = ""):
|
||||||
"""Handler for content update from CodeMirror."""
|
"""Handler for content update from CodeMirror."""
|
||||||
self._state.content = content
|
self._state.content = content
|
||||||
if self._state.auto_save:
|
|
||||||
self.on_content_changed()
|
|
||||||
logger.debug(f"Content updated: {len(content)} chars")
|
logger.debug(f"Content updated: {len(content)} chars")
|
||||||
|
|
||||||
|
if self._state.auto_save:
|
||||||
|
return None, self.on_content_changed() # on_content_changed must be second to benefit from oob swap
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_content(self):
|
||||||
|
logger.debug("save_content")
|
||||||
|
return None, self.on_content_changed() # on_content_changed must be second to benefit from oob swap
|
||||||
|
|
||||||
def toggle_auto_save(self):
|
def toggle_auto_save(self):
|
||||||
|
logger.debug("toggle_auto_save")
|
||||||
self._state.auto_save = not self._state.auto_save
|
self._state.auto_save = not self._state.auto_save
|
||||||
|
logger.debug(f" auto_save={self._state.auto_save}")
|
||||||
return self._mk_auto_save()
|
return self._mk_auto_save()
|
||||||
|
|
||||||
def on_content_changed(self) -> None:
|
def on_content_changed(self) -> None:
|
||||||
@@ -182,7 +191,7 @@ class DslEditor(MultipleInstance):
|
|||||||
mk.button("Save",
|
mk.button("Save",
|
||||||
cls="btn btn-xs btn-primary",
|
cls="btn btn-xs btn-primary",
|
||||||
disabled="disabled" if self._state.auto_save else None,
|
disabled="disabled" if self._state.auto_save else None,
|
||||||
command=self.commands.update_content()),
|
command=self.commands.save_content()),
|
||||||
cls="flex justify-between items-center p-2",
|
cls="flex justify-between items-center p-2",
|
||||||
id=f"as_{self._id}",
|
id=f"as_{self._id}",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ class InstancesDebugger(SingleInstance):
|
|||||||
return self._panel.set_main(vis_network)
|
return self._panel.set_main(vis_network)
|
||||||
|
|
||||||
def on_network_event(self, event_data: dict):
|
def on_network_event(self, event_data: dict):
|
||||||
session, instance_id = event_data["nodes"][0].split("#")
|
parts = event_data["nodes"][0].split("#")
|
||||||
|
session = parts[0]
|
||||||
|
instance_id = "#".join(parts[1:])
|
||||||
properties_def = {"Main": {"Id": "_id", "Parent Id": "_parent._id"},
|
properties_def = {"Main": {"Id": "_id", "Parent Id": "_parent._id"},
|
||||||
"State": {"_name": "_state._name", "*": "_state"},
|
"State": {"_name": "_state._name", "*": "_state"},
|
||||||
"Commands": {"*": "commands"},
|
"Commands": {"*": "commands"},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ from myfasthtml.core.instances import MultipleInstance
|
|||||||
from myfasthtml.icons.fluent_p1 import more_horizontal20_regular
|
from myfasthtml.icons.fluent_p1 import more_horizontal20_regular
|
||||||
from myfasthtml.icons.fluent_p2 import subtract20_regular
|
from myfasthtml.icons.fluent_p2 import subtract20_regular
|
||||||
|
|
||||||
|
logger = logging.getLogger("Panel")
|
||||||
|
|
||||||
class PanelIds:
|
class PanelIds:
|
||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
@@ -116,6 +118,7 @@ class Panel(MultipleInstance):
|
|||||||
return self._ids
|
return self._ids
|
||||||
|
|
||||||
def update_side_width(self, side, width):
|
def update_side_width(self, side, width):
|
||||||
|
logger.debug(f"update_side_width {side=} {width=}")
|
||||||
if side == "left":
|
if side == "left":
|
||||||
self._state.left_width = width
|
self._state.left_width = width
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user